]> code.delx.au - pymsnt/commitdiff
More generic P2P. Avatars almost working in testing suite.
authorjamesbunton <jamesbunton@55fbd22a-6204-0410-b2f0-b6c764c7e90a>
Tue, 6 Dec 2005 12:51:36 +0000 (12:51 +0000)
committerjamesbunton <jamesbunton@55fbd22a-6204-0410-b2f0-b6c764c7e90a>
Tue, 6 Dec 2005 12:51:36 +0000 (12:51 +0000)
git-svn-id: http://delx.cjb.net/svn/pymsnt/trunk@39 55fbd22a-6204-0410-b2f0-b6c764c7e90a

committer: jamesbunton <jamesbunton@55fbd22a-6204-0410-b2f0-b6c764c7e90a>

src/tlib/msn/msn.py
src/tlib/msn/msnft.py
src/tlib/msn/msnp2p.py
src/tlib/msn/test_msn.py

index f106a0bc648868121d9f9df11747b55899d7e2a6..cf502026b3f9eca14e5f1b79238e14c0537f7ae7 100644 (file)
@@ -95,8 +95,6 @@ except ImportError:
         print "Couldn't find a HTTPClient. If you're using Twisted 2.0 make sure you've installed twisted.web"
         raise
 import msnp11chl
-from msnp2p import random_guid
-import msnp2p
 
 # Twisted imports
 from twisted.internet import reactor, task
@@ -110,9 +108,10 @@ from twisted.xish.domish import unescapeFromXml
 from tlib import xmlw
 
 # System imports
-import types, operator, os, sys, base64, random
+import types, operator, os, sys, base64, random, struct, random, sha, base64, StringIO
 from urllib import quote, unquote
 
+
 MSN_PROTOCOL_VERSION = "MSNP11 CVR0"      # protocol version
 MSN_PORT             = 1863               # default dispatch server port
 MSN_MAX_MESSAGE      = 1664               # max message length
@@ -156,6 +155,7 @@ PINGSPEED = 50.0
 
 LINEDEBUG = True
 MESSAGEDEBUG = True
+MSNP2PDEBUG = True
 
 def getVal(inp):
     return inp.split('=')[1]
@@ -185,6 +185,18 @@ def getVals(params):
 
     return userHandle, screenName, userGuid, lists, groups
 
+def b64enc(s):
+    return base64.encodestring(s).replace("\n", "")
+
+def random_guid():
+    format = "{%4X%4X-%4X-%4X-%4X-%4X%4X%4X}"
+    data = []
+    for x in xrange(8):
+        data.append(random.random() * 0xAAFF + 0x1111)
+    data = tuple(data)
+
+    return format % data
+
 def checkParamLen(num, expected, cmd, error=None):
     if error == None: error = "Invalid Number of Parameters for %s" % cmd
     if num != expected: raise MSNProtocolError, error
@@ -416,6 +428,76 @@ class MSNMessage:
         """ set the message text """
         self.message = message
 
+
+class MSNObject:
+    """
+    Used to represent a MSNObject. This can be currently only be an avatar.
+
+    @ivar creator: The userHandle of the creator of this picture.
+    @ivar imageData: The PNG image data (only for our own avatar)
+    @ivar type: Always set to 3, for avatar.
+    @ivar size: The size of the image.
+    @ivar location: The filename of the image.
+    @ivar friendly: Unknown.
+    @ivar text: The textual representation of this MSNObject.
+    """
+    def __init__(self, s=""):
+        """ Pass a XML MSNObject string to parse it, or pass no arguments for a null MSNObject to be created. """
+        if s:
+            self.parse(s)
+        else:
+            self.setNull()
+    
+    def setData(self, creator, imageData):
+        """ Set the creator and imageData for this object """
+        self.creator = creator
+        self.imageData = imageData
+        self.size = len(imageData)
+        self.type = 3
+        self.location = "TMP" + str(random.randint(1000,9999))
+        self.friendly = "AAA="
+        self.sha1d = b64enc(sha.sha(imageData).digest())
+        self.makeText()
+    
+    def setNull(self):
+        self.creator = ""
+        self.imageData = ""
+        self.size = 0
+        self.type = 0
+        self.location = ""
+        self.friendly = ""
+        self.sha1d = ""
+        self.text = ""
+    
+    def makeText(self):
+        """ Makes a textual representation of this MSNObject. Stores it in self.text """
+        h = []
+        h.append("Creator")
+        h.append(self.creator)
+        h.append("Size")
+        h.append(str(self.size))
+        h.append("Type")
+        h.append(str(self.type))
+        h.append("Location")
+        h.append(self.location)
+        h.append("Friendly")
+        h.append(self.friendly)
+        h.append("SHA1D")
+        h.append(self.sha1d)
+        sha1c = b64enc(sha.sha("".join(h)).digest())
+        self.text = '<msnobj Creator="%s" Size="%s" Type="%s" Location="%s" Friendly="%s" SHA1D="%s" SHA1C="%s"/>' % (self.creator, str(self.size), str(self.type), self.location, self.friendly, self.sha1d, sha1c)
+    
+    def parse(self, s):
+        e = xmlw.parseText(s, True)
+        self.creator = e.getAttribute("Creator")
+        self.size = int(e.getAttribute("Size"))
+        self.type = int(e.getAttribute("Type"))
+        self.location = e.getAttribute("Location")
+        self.friendly = e.getAttribute("Friendly")
+        self.sha1d = e.getAttribute("SHA1D")
+        self.text = s
+
+
 class MSNContact:
     
     """
@@ -429,7 +511,7 @@ class MSNContact:
     @ivar lists: An integer representing the sum of all lists
                  that this contact belongs to.
     @ivar caps: int, The capabilities of this client
-    @ivar msnobj: msnp2p.MSNOBJ, The MSNObject representing the contact's avatar
+    @ivar msnobj: The MSNObject representing the contact's avatar
     @ivar status: The contact's status code.
     @type status: str if contact's status is known, None otherwise.
     @ivar personal: The contact's personal message .
@@ -813,7 +895,7 @@ class NotificationClient(MSNEventBase):
         self._state = ['DISCONNECTED', {}]
         self.pingCounter = 0
         self.pingCheckTask = None
-        self.msnobj = msnp2p.MSNOBJ()
+        self.msnobj = MSNObject()
 
     def _setState(self, state):
         self._state[0] = state
@@ -1011,9 +1093,9 @@ class NotificationClient(MSNEventBase):
             self.contactAvatarChanged(msnContact.userHandle, "")
 
     def handleAvatarHelper(self, msnContact, msnobjStr):
-        msnobj = msnp2p.MSNOBJ(unquote(msnobjStr))
+        msnobj = MSNObject(unquote(msnobjStr))
         if not msnContact.msnobj or msnobj.sha1d != msnContact.msnobj.sha1d:
-            if msnp2p.MSNP2P_DEBUG: log.msg("Updated MSNOBJ received!" + msnobjStr)
+            if MSNP2PDEBUG: log.msg("Updated MSNObject received!" + msnobjStr)
             msnContact.msnobj = msnobj
             msnContact.msnobjGot = False
             self.contactAvatarChanged(msnContact.userHandle, msnContact.msnobj.sha1d)
@@ -1907,21 +1989,22 @@ class SwitchboardClient(MSNEventBase):
                      to a switchboard invitation
     @ivar reply: set this to 1 in connectionMade or before to signifiy
                  that you are replying to a switchboard invitation.
+    @ivar msnobj: the MSNObject for the user. So that the switchboard can distribute it.
     """
 
     key = 0
     userHandle = ""
     sessionID = ""
     reply = 0
+    msnobj = None
 
     _iCookie = 0
 
-    def __init__(self, msnobj=None):
+    def __init__(self):
         MSNEventBase.__init__(self)
         self.pendingUsers = {}
         self.cookies = {'iCookies' : {}} # will maybe be moved to a factory in the future
         self.slpLinks = {}
-        self.msnobj = msnobj
 
     def connectionMade(self):
         MSNEventBase.connectionMade(self)
@@ -1968,10 +2051,10 @@ class SwitchboardClient(MSNEventBase):
         self.gotSendRequest(msnft.MSNFTP_Receive(filename, filesize, message.userHandle, cookie, connectivity, self))
         return 1
 
-    def _checkP2PMessage(self, message, cTypes):
+    def _handleP2PMessage(self, message):
         """ helper method for msnslp messages (file transfer & avatars) """
         packet = message.message
-        binaryFields = msnp2p.BinaryFields(packet=packet)
+        binaryFields = BinaryFields(packet=packet)
         if binaryFields[0] != 0:
             slpLink = self.slpLinks[binaryFields[0]]
             if slpLink.remoteUser == message.userHandle:
@@ -1979,16 +2062,22 @@ class SwitchboardClient(MSNEventBase):
         elif binaryFields[5] == BinaryFields.ACK or binaryFields[5] == BinaryFields.BYEGOT:
             pass # Ignore the ACKs
         else:
-            slpMessage = msnp2p.MSNSLPMessage(packet)
+            slpMessage = MSNSLPMessage(packet)
             slpLink = None
             if slpMessage.method == "INVITE":
                 if slpMessage.euf_guid == MSN_MSNFTP_GUID:
-                    slpLink = msnp2p.SLPLink_Receive(slpMessage.fro, slpMessage.sessionID, slpMessage.sessionGuid)
-                    context = msnp2p.FileContext(slpMessage.context)
-                    fileReceive = msnft.MSNP2P_Receive(context.filename, context.filesize, slpMessage.fro, self)
+                    slpLink = SLPLink_Receive(slpMessage.fro, slpMessage.sessionID, slpMessage.sessionGuid)
+                    context = FileContext(slpMessage.context)
+                    fileReceive = msnft.MSNP2P_Receive(context.filename, context.filesize, slpMessage.fro)
                 elif slpMessage.euf_guid == MSN_AVATAR_GUID:
-                    slpLink = msnp2p.SLPLink_Send(slpMessage.fro, slpMessage.sessionID, slpMessage.sessionGuid)
-                    self._sendMSNSLPResponse(slpLink, "200 OK")
+                    # Check that we have an avatar to send
+                    if self.msnobj:
+                        slpLink = SLPLink_Send(slpMessage.fro, self, self.msnobj.size, slpMessage.sessionID, slpMessage.sessionGuid)
+                        self._sendMSNSLPResponse(slpLink, "200 OK")
+                        slpLink.write(self.msnobj.imageData)
+                        slpLink.finish()
+                    else:
+                        pass # FIXME, should send an error response
                 if slpLink:
                     self.slpLinks[slpMessage.sessionID] = slpLink
             else:
@@ -2004,50 +2093,51 @@ class SwitchboardClient(MSNEventBase):
                     slpLink.transferReady()
             if slpLink:
                 # Always need to ACK these packets if we can
-                self._sendP2PACK(self, slpLink, binaryHeaders)
-            
-        return 1
+                self._sendP2PACK(slpLink, binaryFields)
+
 
     def _sendP2PACK(self, slpLink, ackHeaders):
-        binaryFields = msnp2p.BinaryFields()
-        binaryFields[1] = slpLink.nextBaseID()
+        binaryFields = BinaryFields()
+        binaryFields[1] = slpLink.seqID.next()
         binaryFields[3] = ackHeaders[3]
         binaryFields[5] = BinaryFields.ACK
         binaryFields[6] = ackHeaders[1]
         binaryFields[7] = ackHeaders[6]
         binaryFields[8] = ackHeaders[3]
-        self._sendP2PMessage(binaryFields, "")
+        self._sendP2PMessage(binaryFields, "", slpLink.remoteUser)
 
-    def _sendMSNSLPInvite(self, slpLink, guid, context):
-        msg = msnp2p.MSNSLP_Message()
-        msg.create(method="INVITE", to=slpLink.remoteUser, fro=self.userHandle, cseq=0, sessionGuid=slpLink.sessionGuid)
-        msg.setData(sessionID=slpLink.sessionID, appID="1", guid=guid, context=msnp2p.b64enc(context))
+    def _sendMSNSLPCommand(self, command, slpLink, guid, context):
+        msg = MSNSLPMessage()
+        msg.create(method=command, to=slpLink.remoteUser, fro=self.userHandle, cseq=0, sessionGuid=slpLink.sessionGuid)
+        msg.setData(sessionID=slpLink.sessionID, appID="1", guid=guid, context=b64enc(context + chr(0)))
         self._sendMSNSLPMessage(slpLink, msg)
 
     def _sendMSNSLPResponse(self, slpLink, response):
-        msg = msnp2p.MSNSLPMessage()
+        msg = MSNSLPMessage()
         msg.create(status=response, to=slpLink.remoteUser, fro=self.userHandle, cseq=1, sessionGuid=slpLink.sessionGuid)
         msg.setData(sessionID=slpLink.sessionID)
         self._sendMSNSLPMessage(slpLink, msg)
 
-    def _sendMSNSLPMessage(self, slpLink, msnSlpMessage):
-        msgStr = str(msg)
-        binaryFields = msnp2p.BinaryFields()
-        binaryFields[1] = slpLink.nextBaseID()
+    def _sendMSNSLPMessage(self, slpLink, msnSLPMessage):
+        msgStr = str(msnSLPMessage)
+        binaryFields = BinaryFields()
+        binaryFields[1] = slpLink.seqID.next()
         binaryFields[3] = len(msgStr)
         binaryFields[4] = binaryFields[3]
         binaryFields[6] = random.randint(0, sys.maxint)
-        self._sendP2PMessage(binaryFields, msgStr)
+        self._sendP2PMessage(binaryFields, msgStr, msnSLPMessage.to)
 
-    def _sendP2PMessage(self, binaryFields, msgStr):
+    def _sendP2PMessage(self, binaryFields, msgStr, to):
         packet = binaryFields.packHeaders() + msgStr + binaryFields.packFooter()
 
         message = MSNMessage(message=packet)
         message.setHeader("Content-Type", "application/x-msnmsgrp2p")
-        message.setHeader("P2P-Dest", handler.to)
+        message.setHeader("P2P-Dest", to)
         message.ack = MSNMessage.MESSAGE_ACK_FAT
         self.sendMessage(message)
 
+
+
     def checkMessage(self, message):
         """
         hook for detecting any notification type messages
@@ -2064,8 +2154,9 @@ class SwitchboardClient(MSNEventBase):
                     info[key] = val.lstrip()
                 except ValueError: continue
             if self._checkFileInvitation(message, info): return 0
-        if 'application/x-msnmsgrp2p' in cTypes:
-            if self._checkP2PMessage(message, cTypes): return 0
+        elif 'application/x-msnmsgrp2p' in cTypes:
+            self._handleP2PMessage(message)
+            return 0
         return 1
 
     # negotiation
@@ -2155,15 +2246,6 @@ class SwitchboardClient(MSNEventBase):
         """
         pass
 
-    def gotAvatarImage(self, userHandle, image):
-        """
-        called when we receive an avatar from a contact
-
-        @param userHandle: the person who's avatar we have got
-        @param image: the avatar image
-        """
-        pass
-
     def gotSendRequest(self, fileReceive):
         """
         called when we receive a file send request from a contact
@@ -2230,16 +2312,36 @@ class SwitchboardClient(MSNEventBase):
         if MESSAGEDEBUG: log.msg(message.message)
         return d
 
-    def sendAvatarRequest(self, userHandle, msnobj):
-        pass
+    def sendAvatarRequest(self, msnContact):
+        """
+        used to request an avatar from a user in this switchboard
+        session.
+
+        @param msnContact: the msnContact object to request an avatar for
+
+        @return: A Deferred, the callback for which will be called
+                 when the avatar transfer succeeds.
+                 The callback argument will be a tuple with one element,
+                 the PNG avatar data.
+        """
+        if not msnContact.msnobj: return
+        d = Deferred()
+        def bufferClosed(data):
+            d.callback((data,))
+        buffer = StringBuffer(bufferClosed)
+        slpLink = SLPLink_Receive(msnContact.userHandle, self, buffer)
+        slpLink.avatarDataBuffer = buffer
+        self.slpLinks[slpLink.sessionID] = slpLink
+        self._sendMSNSLPCommand("INVITE", slpLink, MSN_AVATAR_GUID, msnContact.msnobj.text)
+        return d
 
     def sendTypingNotification(self):
         """
-        used to send a typing notification. Upon receiving this
+        Used to send a typing notification. Upon receiving this
         message the official client will display a 'user is typing'
         message to all other users in the chat session for 10 seconds.
-        The official client sends one of these every 5 seconds (I think)
-        as long as you continue to type.
+        You should send one of these every 5 seconds as long as the
+        user is typing.
         """
         m = MSNMessage()
         m.ack = m.MESSAGE_ACK_NONE
@@ -2307,6 +2409,340 @@ class SwitchboardClient(MSNEventBase):
         self.sendMessage(m)
 
 
+class BinaryFields:
+    """ Utility class for the binary header & footer in p2p messages """
+    ACK = 0x02
+    WAIT = 0x04
+    ERR = 0x08
+    DATA = 0x20
+    BYEGOT = 0x40
+    BYESENT = 0x80
+    DATAFT = 0x1000030
+
+    def __init__(self, fields=None, packet=None):
+        if fields:
+            self.fields = fields
+        else:
+            self.fields = [0] * 10
+            if packet:
+                self.unpackFields(packet)
+    
+    def __getitem__(self, key):
+        return self.fields[key]
+    
+    def __setitem__(self, key, value):
+        self.fields[key] = value
+    
+    def unpackFields(self, packet):
+        self.fields = struct.unpack("<LLQQLLLLQ", packet[0:48])
+        self.fields += struct.unpack(">L", packet[len(packet)-4:])
+        if MSNP2PDEBUG:
+            print "Unpacked fields:",
+            for i in self.fields:
+                print hex(i),
+            print
+    
+    def packHeaders(self):
+        f = tuple(self.fields)
+        if MSNP2PDEBUG:
+            print "Packed fields:",
+            for i in self.fields:
+                print hex(i),
+            print
+        return struct.pack("<LLQQLLLLQ", f[0], f[1], f[2], f[3], f[4], f[5], f[6], f[7], f[8])
+    
+    def packFooter(self):
+        return struct.pack(">L", self.fields[9])
+    
+
+class MSNSLPMessage:
+    """ Representation of a single MSNSLP message """
+    def __init__(self, packet=None):
+        self.method = ""
+        self.status = ""
+        self.to = ""
+        self.fro = ""
+        self.cseq = 0
+        self.sessionGuid = ""
+        self.sessionID = None
+        self.euf_guid = ""
+        self.data = "\r\n" + chr(0)
+        if packet:
+            self.parse(packet)
+
+    def create(self, method=None, status=None, to=None, fro=None, cseq=0, sessionGuid=None, data=None):
+        self.method = method
+        self.status = status
+        self.to = to
+        self.fro = fro
+        self.cseq = cseq
+        self.sessionGuid = sessionGuid
+        if data: self.data = data
+    
+    def setData(self, sessionID=None, appID=None, guid=None, context=None):
+        s = []
+        if guid:      s.append("EUF-GUID: %s\r\n" % guid)
+        if sessionID: s.append("SessionID: %s\r\n" % sessionID)
+        if appID:     s.append("AppID: %s\r\n" % appID)
+        if context:   s.append("Context: %s\r\n\r\n" % context)
+        s.append(chr(0))
+        
+        self.data = "".join(s)
+    
+    def parse(self, s):
+        s = s[48:len(s)-4:]
+        if s.find("MSNSLP/1.0") < 0: return
+
+        lines = s.split("\r\n")
+        
+        # Get the MSNSLP method or status
+        msnslp = lines[0].split(" ")
+        if MSNP2PDEBUG: print "Parsing MSNSLPMessage", s, len(s)
+        if msnslp[0] in ("INVITE", "BYE"):
+            self.method = msnslp[0].strip()
+        else:
+            self.status = msnslp[1].strip()
+
+        lines.remove(lines[0])
+        
+        for line in lines:
+            line = line.split(":")
+            if len(line) > 1:
+                if len(line) > 2 and line[0] == "To":
+                    self.to = line[2][:line[2].find('>')]
+                elif len(line) > 2 and line[0] == "From":
+                    self.fro = line[2][:line[2].find('>')]
+                elif line[0] == "Call-ID":
+                    self.sessionGuid = line[1].strip()
+                elif line[0] == "CSeq":
+                    self.cseq = int(line[1].strip())
+                elif line[0] == "SessionID":
+                    self.sessionID = int(line[1].strip())
+                elif line[0] == "EUF-GUID":
+                    self.euf_guid = line[1].strip()
+
+    def __str__(self):
+        s = []
+        if self.method:
+            s.append("%s MSNMSGR:%s MSNSLP/1.0\r\n" % (self.method, self.to))
+        else:
+            s.append("MSNSLP/1.0 %s\r\n" % self.status)
+        s.append("To: <msnmsgr:%s>\r\n" % self.to)
+        s.append("From: <msnmsgr:%s>\r\n" % self.fro)
+        s.append("Via: MSNSLP/1.0/TLP ;branch=%s\r\n" % random_guid())
+        s.append("CSeq: %s \r\n" % str(self.cseq))
+        s.append("Call-ID: %s\r\n" % self.sessionGuid)
+        s.append("Max-Forwards: 0\r\n")
+        if self.method == "BYE":
+            s.append("Content-Type: application/x-msnmsgr-sessionclosebody\r\n")
+        else:
+            s.append("Content-Type: application/x-msnmsgr-sessionreqbody\r\n")
+        s.append("Content-Length: %s\r\n\r\n" % len(self.data))
+        s.append(self.data)
+        return "".join(s)
+
+
+class SeqID:
+    """ Utility for handling the weird sequence IDs in p2p messages """
+    def __init__(self, baseID=None):
+        if baseID:
+            self.seqID = baseID
+        else:
+            self.seqID = random.randint(4, 2**30)
+        self.pos = -1
+    
+    def get(self):
+        if self.pos == 0:
+            return self.seqID
+        else:
+            return self.seqID + self.pos - 3
+    
+    def next(self):
+        self.pos += 1
+        if self.pos == 3:
+            self.pos += 1
+        return self.get()
+
+class StringBuffer(StringIO.StringIO):
+    def __init__(self, notifyFunc=None):
+        self.notifyFunc = notifyFunc
+        StringIO.StringIO.__init__(self)
+
+    def finish(self):
+        if self.notifyFunc:
+            self.notifyFunc(self.getvalue())
+            self.notifyFunc = None
+        StringIO.StringIO.close(self)
+
+
+class SLPLink:
+    def __init__(self, remoteUser, switchboard, sessionID, sessionGuid):
+        if not sessionID:
+            sessionID = random.randint(1000, sys.maxint)
+        if not sessionGuid:
+            sessionGuid = random_guid()
+        self.remoteUser = remoteUser
+        self.switchboard = switchboard
+        self.sessionID = sessionID
+        self.sessionGuid = sessionGuid
+        self.seqID = SeqID()
+
+    def transferReady(self):
+        pass
+
+    def setError(self, text):
+        if MSNP2PDEBUG:
+            print "ERROR in avatar transfer: ", self, text, "in state:", self.state
+        self.state = self.ERROR
+    
+    def warn(self, text):
+        if MSNP2PDEBUG:
+            print "Warning in avatar transfer: ", self, text, "in state:", self.state
+    
+
+class SLPLink_Send(SLPLink):
+    def __init__(self, remoteUser, switchboard, length, sessionID=None, sessionGuid=None):
+        SLPLink.__init__(self, remoteUser, switchboard, sessionID, sessionGuid)
+        self.handlePacket = None
+        self.offset = 0
+        self.length = length
+        self.ready = False # Have we received the go ahead to transmit yet?
+        self.data = ""
+        # Send dataprep
+        self.send_dataprep()
+    
+    def send_dataprep(self):
+        binaryFields = BinaryFields()
+        binaryFields[0] = self.sessionID
+        binaryFields[1] = self.seqID.next()
+        binaryFields[3] = 4
+        binaryFields[4] = 4
+        binaryFields[6] = random.randint(0, sys.maxint)
+        binaryFields[9] = 1
+
+        self.switchboard._sendP2PMessage(binaryFields, chr(0) * 4, self.remoteUser)
+
+    def transferReady(self):
+        self.ready = True
+        if self.data:
+            data = self.data
+            self.data = ""
+            self.write(data)
+
+    def write(self, data):
+        if not self.ready:
+            self.data += data
+            return
+
+        i = 0
+        length = len(data)
+        while i < length:
+            if i + 1202 < length:
+                self._writeChunk(data[i:i+1202])
+                i += 1202
+            else:
+                self.data += data[i:]
+                if len(self.data) >= 1202:
+                    data = self.data
+                    self.data = ""
+                    self.write(data)
+                return
+
+    def _writeChunk(self, chunk):
+        binaryFields = BinaryFields()
+        binaryFields[0] = self.sessionID
+        binaryFields[1] = self.seqID.next()
+        binaryFields[2] = self.offset
+        binaryFields[3] = self.length
+        binaryFields[4] = len(chunk)
+        binaryFields[5] = BinaryFields.DATA
+        binaryFields[6] = random.randint(0, 2**30)
+        binaryFields[9] = 1
+
+        self.offset += len(chunk)
+        self.switchboard._sendP2PMessage(binaryFields, chunk, self.remoteUser)
+    
+    def finish(self):
+        if self.data:
+            self._writeChunk(self.data)
+        self.switchboard = None
+        # FIXME Now tell the switchboard
+    
+    def error(self):
+        pass
+        # Same as above?
+
+
+class SLPLink_Receive(SLPLink):
+    def __init__(self, remoteUser, switchboard, consumer, sessionID=None, sessionGuid=None):
+        SLPLink.__init__(self, remoteUser, switchboard, sessionID, sessionGuid)
+        self.consumer = consumer
+        self.pos = 0
+
+        self.handlePacket = self.wait_dataprep
+    
+    def wait_dataprep(self, packet):
+        binaryFields = BinaryFields()
+        binaryFields.unpackFields(packet)
+
+        if binaryFields[0] != self.sessionID:
+            self.warn("field0," + str(binaryFields[0]) + "," + str(self.sessionID))
+            return
+        if binaryFields[3] != 4:
+            self.setError("field3," + str(binaryFields[3]))
+            return
+        if binaryFields[4] != 4:
+            self.setError("field4," + str(binaryFields[4]))
+            return
+        if binaryFields[9] != 1:
+            self.warn("field9," + str(binaryFields[9]))
+        #   return
+
+        self.switchboard._sendP2PACK(self, binaryFields)
+        self.handlePacket = self.wait_data
+
+    def wait_data(self, packet):
+        binaryFields = BinaryFields()
+        binaryFields.unpackFields(packet)
+        if binaryFields[0] != self.sessionID:
+            self.warn("field0," + str(binaryFields[0]) + "," + str(self.sessionID))
+            return
+        if binaryFields[5] != BinaryFields.DATA:
+            self.setError("field5," + str(binaryFields[5]))
+            return
+        if binaryFields[9] != 1:
+            self.warn("field9," + str(binaryFields[9]))
+        #   return
+        offset = binaryFields[2]
+        total = binaryFields[3]
+        length = binaryFields[4]
+
+        data = packet[48:-4]
+        if offset != self.pos:
+            self.setError("Received packet out of order")
+            self.consumer.error()
+            return
+        if len(data) != length:
+            self.setError("Received bad length of slp")
+            self.consumer.error()
+            return
+        
+        self.pos += length
+
+        self.consumer.write(data)
+
+        if self.pos == total:
+            self.switchboard._sendP2PACK(self, binaryFields)
+            self.consumer.finish()
+            self.handlePacket = None
+            del self.switchboard.slpLinks[self.sessionID]
+            self.switchboard = None
+
+
+
+
+
+
 # mapping of error codes to error messages
 errorCodes = {
 
index 28f3662a71034b29d43b8e9a778b3f582d23770c..5bb0eaa65e1fd6cda6a275966545818934acb615 100644 (file)
@@ -20,47 +20,47 @@ MAXAUTHCOOKIE = 2**32-1
 class MSNFTReceive_Base:
        # Public
        def __init__(self, filename, filesize, userHandle):
-               self.connector = None
+               self.consumer = None
                self.finished = False
                self.error = False
                self.buffer = []
                self.filename, self.filesize, self.userHandle = filename, filesize, userHandle
        
        def removeMe(self):
-               self.connector = None
+               self.consumer = None
        
        def accept(self, yes=True):
                pass
        
        def writeTo(self, obj):
-               self.connector = obj
+               self.consumer = obj
                for data in self.buffer:
-                       self.connector.write(data)
+                       self.consumer.write(data)
                self.buffer = []
                if self.finished:
-                       self.connector.close()
+                       self.consumer.close()
                if self.error:
-                       self.connector.error()
+                       self.consumer.error()
 
 
        # Private
        def write(self, data):
-               if self.connector:
-                       self.connector.write(data)
+               if self.consumer:
+                       self.consumer.write(data)
                else:
                        self.buffer.append(data)
        
        def close(self):
                self.removeMe()
                self.finished = True
-               if self.connector:
-                       self.connector.close()
+               if self.consumer:
+                       self.consumer.close()
        
        def gotError(self, ignored=None):
                self.removeMe()
                self.error = True
-               if self.connector:
-                       self.connector.error()
+               if self.consumer:
+                       self.consumer.error()
 
 
 class MSNFTP_Ports:
index 1eed0a7ccbd47e849fc25f56d91355692ce003bf..04874e8ea15733c74f5baebe3c4bc0d5ff300382 100644 (file)
@@ -5,6 +5,7 @@ import struct
 import random
 import sha
 import base64
+import sys
 
 from tlib import xmlw
 
@@ -14,13 +15,13 @@ def b64enc(s):
        return base64.encodestring(s).replace("\n", "")
 
 def random_guid():
-    format = "{%4X%4X-%4X-%4X-%4X-%4X%4X%4X}"
-    data = []
-    for x in xrange(8):
-        data.append(random.random() * 0xAAFF + 0x1111)
-    data = tuple(data)
+       format = "{%4X%4X-%4X-%4X-%4X-%4X%4X%4X}"
+       data = []
+       for x in xrange(8):
+               data.append(random.random() * 0xAAFF + 0x1111)
+       data = tuple(data)
 
-    return format % data
+       return format % data
 
 
 class MSNOBJ:
@@ -155,9 +156,9 @@ class MSNSLPMessage:
        
        def setData(self, sessionID=None, appID=None, guid=None, context=None):
                s = []
-               if guid:      s.append("EUF-GUID: %s\r\n" % guid)
+               if guid:          s.append("EUF-GUID: %s\r\n" % guid)
                if sessionID: s.append("SessionID: %s\r\n" % sessionID)
-               if appID:     s.append("AppID: %s\r\n" % appID)
+               if appID:         s.append("AppID: %s\r\n" % appID)
                if context:   s.append("Context: %s\r\n\r\n" % context)
                s.append(chr(0))
                
@@ -240,27 +241,6 @@ class BaseID:
                        self.pos += 1
                return self.get()
 
-class FileData:
-       def __init__(self, size):
-               self.data = chr(0) * size
-               self.totalSize = size
-               self.gotSize = 0
-       
-       def put(self, offset, data):
-               self.data = self.data[0:offset] + data + self.data[offset+len(data):]
-               self.gotSize += len(data)
-       
-       def finished(self):
-               return self.gotSize >= self.totalSize
-       
-
-#class SLPLink:
-       
-
-
-#class SLPLink_Receive:
-       
-
 class MSNP2P_Avatar:
        TRANSFER_COUNT = 0
        ERROR_COUNT = 0
@@ -726,3 +706,232 @@ class MSNP2P_Avatar_Receive(MSNP2P_Avatar):
 
 
 
+class SLPLink:
+       def __init__(self, remoteUser, switchboard, sessionID, sessionGuid):
+               if not sessionID:
+                       sessionID = random.randint(1000, sys.maxint)
+               if not sessionGuid:
+                       sessionGuid = random_guid()
+               self.remoteUser = remoteUser
+               self.switchboard = switchboard
+               self.sessionID = sessionID
+               self.sessionGuid = sessionGuid
+
+       def setError(self, text):
+               if MSNP2P_DEBUG:
+                       print "ERROR in avatar transfer: ", self, text, "in state:", self.state
+               self.state = self.ERROR
+       
+       def warn(self, text):
+               if MSNP2P_DEBUG:
+                       print "Warning in avatar transfer: ", self, text, "in state:", self.state
+       
+
+class SLPLink_Send(SLPLink):
+       def __init__(self, remoteUser, switchboard, length, sessionID=None, sessionGuid=None):
+               SLPLink.__init__(self, remoteUser, switchboard, sessionID, sessionGuid)
+               self.handlePacket = None
+               self.offset = 0
+               self.length = length
+               self.data = ""
+               # Send dataprep
+               self.send_dataprep()
+       
+       def send_dataprep(self):
+               binaryFields = BinaryFields()
+               binaryFields[0] = self.sessionID
+               binaryFields[1] = self.baseID.next()
+               binaryFields[3] = 4
+               binaryFields[4] = 4
+               binaryFields[6] = random.randint(0, sys.maxint)
+               binaryFields[9] = 1
+
+               self.switchboard._sendP2PMessage(binaryFields, chr(0) * 4)
+
+       def write(self, data):
+               i = 0
+               length = len(data)
+               while i < length:
+                       if i + 1202 < length:
+                               self._writeChunk(data[i:i+1202])
+                               i += 1202
+                       else:
+                               self.data += data
+                               if len(self.data) >= 1202:
+                                       data = self.data
+                                       self.data = ""
+                                       self.write(data)
+                               return
+
+       def _writeChunk(self, chunk):
+               binaryFields = BinaryFields()
+               binaryFields[0] = self.sessionID
+               binaryFields[1] = self.baseID.get()
+               binaryFields[2] = self.fileOffset
+               binaryFields[3] = len(chunk)
+               binaryFields[4] = self.length
+               binaryFields[5] = BinaryFields.DATA
+               binaryFields[6] = random.randint(0, 2**30)
+               binaryFields[9] = 1
+
+               self.offset += len(chunk)
+               self.switchboard._sendP2PMessage(binaryFields, chunk)
+       
+       def finish(self):
+               self._writeChunk(self.data)
+               # FIXME Now tell the switchboard
+       
+       def error(self):
+               pass
+               # Same as above?
+
+
+class SLPLink_Receive(SLPLink):
+       def __init__(self, remoteUser, switchboard, consumer, sessionID=None, sessionGuid=None):
+               SLPLink.__init__(self, remoteUser, switchboard, sessionID, sessionGuid)
+               self.consumer = consumer
+               self.pos = 0
+
+               self.handlePacket = wait_dataprep
+       
+       def wait_dataprep(self, packet):
+               binaryFields = BinaryFields()
+               binaryFields.unpackFields(packet)
+
+               if binaryFields[0] != self.sessionID:
+                       self.warn("field0," + str(binaryFields[0]) + "," + str(self.sessionID))
+                       return
+               if binaryFields[3] != 4:
+                       self.setError("field3," + str(binaryFields[3]))
+                       return
+               if binaryFields[4] != 4:
+                       self.setError("field4," + str(binaryFields[4]))
+                       return
+               if binaryFields[9] != 1:
+                       self.warn("field9," + str(binaryFields[9]))
+               #       return
+
+               self.switchboard._sendP2PACK(self, binaryFields)
+               self.handlePacket = self.wait_data
+
+       def wait_data(self, packet):
+               binaryFields = BinaryFields()
+               binaryFields.unpackFields(packet)
+               if binaryFields[0] != self.sessionID:
+                       self.warn("field0," + str(binaryFields[0]) + "," + str(self.sessionID))
+                       return
+               if binaryFields[5] != BinaryFields.DATA:
+                       self.setError("field5," + str(binaryFields[5]))
+                       return
+               if binaryFields[9] != 1):
+                       self.warn("field9," + str(binaryFields[9]))
+               #       return
+               offset = binaryFields[2]
+               total = binaryFields[3]
+               length = binaryFields[4]
+
+               data = packet[48:-4]
+               if offset != self.pos:
+                       self.setError("Received packet out of order")
+                       self.consumer.error()
+                       return
+               if len(data) != length:
+                       self.setError("Received bad length of slp")
+                       self.consumer.error()
+                       return
+               
+               self.pos += length
+
+               self.consumer.write(data)
+
+               if self.pos == total:
+                       self.switchboard._sendP2PACK(self, binaryFields)
+                       self.consumer.finish()
+                       self.switchboard.gotAvatarImage(self.remoteUser, self.fileData.data)
+                       self.handlePacket = None
+                       del self.switchboard.slpLinks[self.sessionID]
+
+
+
+
+class SwitchboardP2PMixin:
+       def handleP2PMessage(self, message, cTypes):
+               """ helper method for msnslp messages (file transfer & avatars) """
+               packet = message.message
+               binaryFields = msnp2p.BinaryFields(packet=packet)
+               if binaryFields[0] != 0:
+                       slpLink = self.slpLinks[binaryFields[0]]
+                       if slpLink.remoteUser == message.userHandle:
+                               slpLink.handlePacket(packet)
+               elif binaryFields[5] == BinaryFields.ACK or binaryFields[5] == BinaryFields.BYEGOT:
+                       pass # Ignore the ACKs
+               else:
+                       slpMessage = msnp2p.MSNSLPMessage(packet)
+                       slpLink = None
+                       if slpMessage.method == "INVITE":
+                               if slpMessage.euf_guid == MSN_MSNFTP_GUID:
+                                       slpLink = msnp2p.SLPLink_Receive(slpMessage.fro, slpMessage.sessionID, slpMessage.sessionGuid)
+                                       context = msnp2p.FileContext(slpMessage.context)
+                                       fileReceive = msnft.MSNP2P_Receive(context.filename, context.filesize, slpMessage.fro)
+                               elif slpMessage.euf_guid == MSN_AVATAR_GUID:
+                                       slpLink = msnp2p.SLPLink_Send(slpMessage.fro, slpMessage.sessionID, slpMessage.sessionGuid)
+                                       self._sendMSNSLPResponse(slpLink, "200 OK")
+                               if slpLink:
+                                       self.slpLinks[slpMessage.sessionID] = slpLink
+                       else:
+                               if slpMessage.status != "200":
+                                       for slpLink in self.slpLinks:
+                                               if slpLink.sessionGuid == slpMessage.sessionGuid:
+                                                       del self.slpLinks[slpLink.sessionID]
+                                       if slpMessage.method != "BYE":
+                                               # Must be an error. If its a file transfer we need to signal that it failed
+                                               slpLink.transferError()
+                               else:
+                                       slpLink = self.slpLinks[slpMessage.sessionID]
+                                       slpLink.transferReady()
+                       if slpLink:
+                               # Always need to ACK these packets if we can
+                               self._sendP2PACK(self, slpLink, binaryHeaders)
+
+
+       def _sendP2PACK(self, slpLink, ackHeaders):
+               binaryFields = msnp2p.BinaryFields()
+               binaryFields[1] = slpLink.nextBaseID()
+               binaryFields[3] = ackHeaders[3]
+               binaryFields[5] = BinaryFields.ACK
+               binaryFields[6] = ackHeaders[1]
+               binaryFields[7] = ackHeaders[6]
+               binaryFields[8] = ackHeaders[3]
+               self._sendP2PMessage(binaryFields, "")
+
+       def _sendMSNSLPCommand(self, command, slpLink, guid, context):
+               msg = msnp2p.MSNSLP_Message()
+               msg.create(method=command, to=slpLink.remoteUser, fro=self.userHandle, cseq=0, sessionGuid=slpLink.sessionGuid)
+               msg.setData(sessionID=slpLink.sessionID, appID="1", guid=guid, context=msnp2p.b64enc(context + chr(0)))
+               self._sendMSNSLPMessage(slpLink, msg)
+
+       def _sendMSNSLPResponse(self, slpLink, response):
+               msg = msnp2p.MSNSLPMessage()
+               msg.create(status=response, to=slpLink.remoteUser, fro=self.userHandle, cseq=1, sessionGuid=slpLink.sessionGuid)
+               msg.setData(sessionID=slpLink.sessionID)
+               self._sendMSNSLPMessage(slpLink, msg)
+
+       def _sendMSNSLPMessage(self, slpLink, msnSlpMessage):
+               msgStr = str(msg)
+               binaryFields = msnp2p.BinaryFields()
+               binaryFields[1] = slpLink.nextBaseID()
+               binaryFields[3] = len(msgStr)
+               binaryFields[4] = binaryFields[3]
+               binaryFields[6] = random.randint(0, sys.maxint)
+               self._sendP2PMessage(binaryFields, msgStr)
+
+       def _sendP2PMessage(self, binaryFields, msgStr):
+               packet = binaryFields.packHeaders() + msgStr + binaryFields.packFooter()
+
+               message = MSNMessage(message=packet)
+               message.setHeader("Content-Type", "application/x-msnmsgrp2p")
+               message.setHeader("P2P-Dest", handler.to)
+               message.ack = MSNMessage.MESSAGE_ACK_FAT
+               self.sendMessage(message)
+
+
index fb3e0930b159116fbaf8ebc57e8f9e4ab0e97966..237a59407274aa47d8a0a5091ba4b0bc60d55e76 100644 (file)
@@ -987,6 +987,30 @@ class SwitchboardP2PTests(unittest.TestCase):
         self.loop1.doSteps(10)
         self.loop2.doSteps(10)
         self.failUnless((self.client2.status == "GOTMESSAGE"), "Fake switchboard server not working.")
+    
+    def testAvatars(self):
+        self.gotAvatar = False
+
+        # Set up the avatar for client1
+        imageData = 'avatar image data' * 1000
+        self.client1.msnobj = msn.MSNObject()
+        self.client1.msnobj.setData('foo1@bar.com', imageData)
+        self.client1.msnobj.makeText()
+
+        # Make client2 request the avatar
+        def avatarCallback((data,)):
+            self.gotAvatar = (data == imageData)
+        msnContact = msn.MSNContact(userHandle='foo1@bar.com', msnobj=self.client1.msnobj)
+        d = self.client2.sendAvatarRequest(msnContact)
+        d.addCallback(avatarCallback)
+
+        # Let them do their thing
+        for i in xrange(100):
+            self.loop1.doSteps(1)
+            self.loop2.doSteps(1)
+
+        # Check that client2 got the avatar
+        self.failUnless((self.gotAvatar), "Failed to transfer avatar")
 
 
 ################