probably a good idea to submit a bug report. ;)
Use of this module requires that PyOpenSSL is installed.
-TODO
-====
-- check message hooks with invalid x-msgsinvite messages.
-- font handling
-- switchboard factory
-
@author: U{Sam Jordan<mailto:sam@twistedmatrix.com>}
+@author: U{James Bunton<mailto:james@delx.cjb.net>}
"""
from __future__ import nested_scopes
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
from twisted.internet.defer import Deferred
-from twisted.internet.protocol import ClientFactory
-from twisted.internet.ssl import ClientContextFactory
+from twisted.internet.protocol import ReconnectingClientFactory, ClientFactory
+try:
+ from twisted.internet.ssl import ClientContextFactory
+except ImportError:
+ print "You must install pycrypto and pyopenssl."
+ raise
from twisted.python import failure, log
-from twisted.xish.domish import unescapeFromXml
# Compat stuff
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, array, codecs, binascii
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
STATUS_PHONE = 'PHN'
STATUS_LUNCH = 'LUN'
-CR = "\r"
-LF = "\n"
PINGSPEED = 50.0
-LINEDEBUG = True
-MESSAGEDEBUG = True
+DEBUGALL = True
+LINEDEBUG = False
+MESSAGEDEBUG = False
+MSNP2PDEBUG = False
+
+if DEBUGALL:
+ LINEDEBUG = True
+ MESSAGEDEBUG = True
+ MSNP2PDEBUG = True
+
+
+P2PSEQ = [-3, -2, 0, -1, 1, 2, 3, 4, 5, 6, 7, 8]
+def p2pseq(n):
+ if n > 5:
+ return n - 3
+ else:
+ return P2PSEQ[n]
+
def getVal(inp):
return inp.split('=')[1]
return userHandle, screenName, userGuid, lists, groups
+def ljust(s, n, c):
+ """ Needed for Python 2.3 compatibility """
+ return s + (n-len(s))*c
+
+if sys.byteorder == "little":
+ def utf16net(s):
+ """ Encodes to utf-16 and ensures network byte order. Strips the BOM """
+ a = array.array("h", s.encode("utf-16")[2:])
+ a.byteswap()
+ return a.tostring()
+else:
+ def utf16net(s):
+ """ Encodes to utf-16 and ensures network byte order. Strips the BOM """
+ return s.encode("utf-16")[2:]
+
+def b64enc(s):
+ return base64.encodestring(s).replace("\n", "")
+
+def b64dec(s):
+ for pad in ["", "=", "==", "A", "A=", "A=="]: # Stupid MSN client!
+ try:
+ return base64.decodestring(s + pad)
+ except:
+ pass
+ raise ValueError("Got some very bad base64!")
+
+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
def connectionMade(self):
self.sendCommand('GET', self.path)
self.sendHeader('Authorization', 'Passport1.4 OrgVerb=GET,OrgURL=http://messenger.msn.com,' +
- 'sign-in=%s,pwd=%s,%s' % (quote(self.userHandle), self.passwd,self.authData))
+ 'sign-in=%s,pwd=%s,%s' % (quote(self.userHandle), quote(self.passwd), self.authData))
self.sendHeader('Host', self.host)
self.endHeaders()
self.headers = {}
self.screenName = screenName
self.specialMessage = specialMessage
self.message = message
- self.headers = {'MIME-Version' : '1.0', 'Content-Type' : 'text/plain'}
+ self.headers = {'MIME-Version' : '1.0', 'Content-Type' : 'text/plain; charset=UTF-8'}
self.length = length
self.readPos = 0
"""
return reduce(operator.add, [len(x[0]) + len(x[1]) + 4 for x in self.headers.items()]) + len(self.message) + 2
+ def delHeader(self, header):
+ """ delete the desired header """
+ if self.headers.has_key(header):
+ del self.headers[header]
+
def setHeader(self, header, value):
""" set the desired header """
self.headers[header] = value
""" 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:
"""
@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 .
raise NotImplementedError
def sendLine(self, line):
- if LINEDEBUG: log.msg(">> " + line)
+ if LINEDEBUG: log.msg("<< " + line)
LineReceiver.sendLine(self, line)
def lineReceived(self, line):
- if LINEDEBUG: log.msg("<< " + line)
+ if LINEDEBUG: log.msg(">> " + line)
+ if not self.connected: return
if self.currentMessage:
- self.currentMessage.readPos += len(line+CR+LF)
+ self.currentMessage.readPos += len(line+"\r\n")
try:
header, value = line.split(':')
self.currentMessage.setHeader(header, unquote(value).lstrip())
if len(cmd) != 3: raise MSNProtocolError, "Invalid Command, %s" % repr(cmd)
if cmd.isdigit():
- if self.ids.has_key(params.split(' ')[0]):
- self.ids[id].errback(int(cmd))
+ id = params.split(' ')[0]
+ if id.isdigit() and self.ids.has_key(int(id)):
+ id = int(id)
+ self.ids[id][0].errback(int(cmd))
del self.ids[id]
return
else: # we received an error which doesn't map to a sent command
self.handle_UNKNOWN(cmd, params.split(' '))
def rawDataReceived(self, data):
+ if not self.connected: return
extra = ""
self.currentMessage.readPos += len(data)
diff = self.currentMessage.readPos - self.currentMessage.length
m = self.currentMessage
self.currentMessage = None
if MESSAGEDEBUG: log.msg(m.message)
- if not self.checkMessage(m):
+ try:
+ if not self.checkMessage(m):
+ self.setLineMode(extra)
+ return
+ except Exception, e:
self.setLineMode(extra)
- return
+ raise
self.gotMessage(m)
self.setLineMode(extra)
"""
log.msg('Error %s' % (errorCodes[errorCode]))
+
class DispatchClient(MSNEventBase):
"""
This class provides support for clients connecting to the dispatch server
@ivar userHandle: your user handle (passport) needed before connecting.
"""
- # eventually this may become an attribute of the
- # factory.
- userHandle = ""
-
def connectionMade(self):
MSNEventBase.connectionMade(self)
self.sendLine('VER %s %s' % (self._nextTransactionID(), MSN_PROTOCOL_VERSION))
self.transport.loseConnection()
raise MSNProtocolError, "Invalid version response"
id = self._nextTransactionID()
- self.sendLine("CVR %s %s %s" % (id, MSN_CVR_STR, self.userHandle))
+ self.sendLine("CVR %s %s %s" % (id, MSN_CVR_STR, self.factory.userHandle))
def handle_CVR(self, params):
- self.sendLine("USR %s TWN I %s" % (self._nextTransactionID(), self.userHandle))
+ self.sendLine("USR %s TWN I %s" % (self._nextTransactionID(), self.factory.userHandle))
def handle_XFR(self, params):
if len(params) < 4: raise MSNProtocolError, "Invalid number of parameters for XFR"
pass
+class DispatchFactory(ClientFactory):
+ """
+ This class keeps the state for the DispatchClient.
+
+ @ivar userHandle: the userHandle to request a notification
+ server for.
+ """
+ protocol = DispatchClient
+ userHandle = ""
+
+
+
class NotificationClient(MSNEventBase):
"""
This class provides support for clients connecting
self._state = ['DISCONNECTED', {}]
self.pingCounter = 0
self.pingCheckTask = None
- self.msnobj = msnp2p.MSNOBJ()
+ self.msnobj = MSNObject()
def _setState(self, state):
self._state[0] = state
MSNEventBase.connectionMade(self)
self._setState('CONNECTED')
self.sendLine("VER %s %s" % (self._nextTransactionID(), MSN_PROTOCOL_VERSION))
+ self.factory.resetDelay()
def connectionLost(self, reason):
self._setState('DISCONNECTED')
self.gotMSNAlert(bodytext, actionurl, subscrurl)
def _gotUBX(self, message):
+ msnContact = self.factory.contacts.getContact(message.userHandle)
+ if not msnContact: return
lm = message.message.lower()
p1 = lm.find("<psm>") + 5
p2 = lm.find("</psm>")
if p1 >= 0 and p2 >= 0:
- personal = unescapeFromXml(message.message[p1:p2])
- msnContact = self.factory.contacts.getContact(message.userHandle)
- if not msnContact: return
+ personal = xmlw.unescapeFromXml(message.message[p1:p2])
msnContact.personal = personal
self.contactPersonalChanged(message.userHandle, personal)
else:
+ msnContact.personal = ''
self.contactPersonalChanged(message.userHandle, '')
def checkMessage(self, message):
self.handleAvatarHelper(msnContact, params[5])
else:
self.handleAvatarGoneHelper(msnContact)
- self.gotContactStatus(params[1], params[2], unquote(params[3]))
+ self.gotContactStatus(params[2], params[1], unquote(params[3]))
def handleAvatarGoneHelper(self, msnContact):
if msnContact.msnobj:
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)
+ self.contactAvatarChanged(msnContact.userHandle, binascii.hexlify(b64dec(msnContact.msnobj.sha1d)))
def handle_CHL(self, params):
checkParamLen(len(params), 2, 'CHL')
self.handleAvatarHelper(msnContact, params[4])
else:
self.handleAvatarGoneHelper(msnContact)
- self.contactStatusChanged(params[0], params[1], unquote(params[2]))
+ self.contactStatusChanged(params[1], params[0], unquote(params[2]))
def handle_FLN(self, params):
checkParamLen(len(params), 1, 'FLN')
def handle_PRP(self, params):
if params[1] == "MFN":
- self._fireCallback(int(params[0]), unquote(params[2]))
+ self._fireCallback(int(params[0]))
elif self._getState() == 'SYNC':
self._getStateData('phone').append((params[0], unquote(params[1])))
else:
self._fireCallback(int(params[0]), int(params[1]), unquote(params[3]))
def handle_BPR(self, params):
- if not self.factory.contacts: raise MSNProtocolError, "handle_BPR called with no contact list" # debug
numParams = len(params)
if numParams == 2: # part of a syn
self._getStateData('last_contact').setPhone(params[0], unquote(params[1]))
elif numParams == 4:
+ if not self.factory.contacts: raise MSNProtocolError, "handle_BPR called with no contact list" # debug
self.factory.contacts.version = int(params[0])
userHandle, phoneType, number = params[1], params[2], unquote(params[3])
self.factory.contacts.getContact(userHandle).setPhone(phoneType, number)
def handle_ADC(self, params):
if not self.factory.contacts: raise MSNProtocolError, "handle_ADC called with no contact list"
numParams = len(params)
- if numParams < 3 or params[1].upper() not in ('AL','BL','RL','FL', 'PL'):
+ if numParams < 3 or params[1].upper() not in ('AL','BL','RL','FL','PL'):
raise MSNProtocolError, "Invalid Paramaters for ADC" # debug
id = int(params[0])
listType = params[1].lower()
def handle_REM(self, params):
if not self.factory.contacts: raise MSNProtocolError, "handle_REM called with no contact list available!"
numParams = len(params)
- if numParams < 3 or params[1].upper() not in ('AL','BL','FL','RL'):
+ if numParams < 3 or params[1].upper() not in ('AL','BL','FL','RL','PL'):
raise MSNProtocolError, "Invalid Paramaters for REM" # debug
id = int(params[0])
listType = params[1].lower()
self.currentMessage = MSNMessage(length=messageLen, userHandle=params[0], screenName="UBX", specialMessage=True)
self.setRawMode()
else:
- self.contactPersonalChanged(params[0], '')
+ self._gotUBX(MSNMessage(userHandle=params[0]))
def handle_UUX(self, params):
checkParamLen(len(params), 2, 'UUX')
def handle_OUT(self, params):
checkParamLen(len(params), 1, 'OUT')
+ self.factory.stopTrying()
if params[0] == "OTH": self.multipleLogin()
elif params[0] == "SSD": self.serverGoingDown()
else: raise MSNProtocolError, "Invalid Parameters received for OUT" # debug
contact.
@param userHandle: contact who's msnobj has been changed
- @param hash: sha1 hash of their avatar
+ @param hash: sha1 hash of their avatar as hex string
"""
def statusChanged(self, statusCode):
"""
pass
- def gotContactStatus(self, statusCode, userHandle, screenName):
+ def gotContactStatus(self, userHandle, statusCode, screenName):
"""
Called when we receive a list of statuses upon login.
- @param statusCode: 3-letter status code
@param userHandle: the contact's user handle (passport)
+ @param statusCode: 3-letter status code
@param screenName: the contact's screen name
"""
pass
- def contactStatusChanged(self, statusCode, userHandle, screenName):
+ def contactStatusChanged(self, userHandle, statusCode, screenName):
"""
Called when we're notified that a contact's status has changed.
- @param statusCode: 3-letter status code
@param userHandle: the contact's user handle (passport)
+ @param statusCode: 3-letter status code
@param screenName: the contact's screen name
"""
pass
c = self.factory.contacts.getContact(r[2])
if not c:
c = MSNContact(userGuid=r[1], userHandle=r[2], screenName=r[3])
+ self.factory.contacts.addContact(c)
#if r[3]: c.groups.append(r[3])
c.addToList(r[0])
return r
@return: A Deferred, the callback for which will be called
when the server acknowledges the change.
- The callback argument will be a tuple of 1 element,
- the new screen name.
+ The callback argument will be an empty tuple.
"""
id, d = self._createIDMapping()
self.sendLine("PRP %s MFN %s" % (id, quote(newName)))
def _cb(r):
- self.factory.screenName = r[0]
+ self.factory.screenName = newName
return r
return d.addCallback(_cb)
if self.pingCheckTask:
self.pingCheckTask.stop()
self.pingCheckTask = None
+ self.factory.stopTrying()
self.sendLine("OUT")
+ self.transport.loseConnection()
-class NotificationFactory(ClientFactory):
+class NotificationFactory(ReconnectingClientFactory):
"""
Factory for the NotificationClient protocol.
This is basically responsible for keeping
(the whole URL is required)
@ivar status: The status of the client -- this is generally kept
up to date by the default command handlers
+ @ivar maxRetries: The number of times the factory will reconnect
+ if the connection dies because of a network error.
"""
contacts = None
screenName = ''
password = ''
passportServer = 'https://nexus.passport.com/rdr/pprdr.asp'
- status = 'FLN'
+ status = 'NLN'
protocol = NotificationClient
+ maxRetries = 5
-# XXX: A lot of the state currently kept in
-# instances of SwitchboardClient is likely to
-# be moved into a factory at some stage in the
-# future
-
class SwitchboardClient(MSNEventBase):
"""
This class provides support for clients connecting to a switchboard server.
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's avatar. So that the
+ switchboard can distribute it to anyone who asks.
"""
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)
def _checkTyping(self, message, cTypes):
""" helper method for checkMessage """
if 'text/x-msmsgscontrol' in cTypes and message.hasHeader('TypingUser'):
- self.userTyping(message)
+ self.gotContactTyping(message)
return 1
def _checkFileInvitation(self, message, info):
cookie = info['Invitation-Cookie']
filename = info['Application-File']
filesize = int(info['Application-FileSize'])
- connectivity = (info.get('Connectivity').lower() == 'y')
+ connectivity = (info.get('Connectivity', 'n').lower() == 'y')
except KeyError:
log.msg('Received munged file transfer request ... ignoring.')
return 0
- import msnft
+ raise NotImplementedError
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) """
+ if not message.getHeader("P2P-Dest") == self.userHandle: return
packet = message.message
- binaryFields = msnp2p.BinaryFields(packet=packet)
- if binaryFields[0] != 0:
- slpLink = self.slpLinks[binaryFields[0]]
+ binaryFields = BinaryFields(packet=packet)
+ if binaryFields[5] == BinaryFields.BYEGOT:
+ pass # Ignore the ACKs to SLP messages
+ elif binaryFields[0] != 0:
+ slpLink = self.slpLinks.get(binaryFields[0])
+ if not slpLink:
+ # Link has been killed. Ignore
+ return
if slpLink.remoteUser == message.userHandle:
slpLink.handlePacket(packet)
- elif binaryFields[5] == BinaryFields.ACK or binaryFields[5] == BinaryFields.BYEGOT:
- pass # Ignore the ACKs
+ elif binaryFields[5] == BinaryFields.ACK:
+ pass # Ignore the ACKs to SLP messages
else:
- slpMessage = msnp2p.MSNSLPMessage(packet)
+ slpMessage = MSNSLPMessage(packet)
slpLink = None
- if slpMessage.method == "INVITE":
+ # Always try and give a slpMessage to a slpLink first.
+ # If none can be found, and it was INVITE, then create
+ # one to handle the session.
+ for slpLink in self.slpLinks.values():
+ if slpLink.sessionGuid == slpMessage.sessionGuid:
+ slpLink.handleSLPMessage(slpMessage)
+ break
+ else:
+ slpLink = None # Was not handled
+
+ if not slpLink and 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)
+ context = FileContext(slpMessage.context)
+ slpLink = SLPLink_FileReceive(remoteUser=slpMessage.fro, switchboard=self, filename=context.filename, filesize=context.filesize, sessionID=slpMessage.sessionID, sessionGuid=slpMessage.sessionGuid, branch=slpMessage.branch)
+ self.slpLinks[slpMessage.sessionID] = slpLink
+ self.gotFileReceive(slpLink)
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_AvatarSend(remoteUser=slpMessage.fro, switchboard=self, filesize=self.msnobj.size, sessionID=slpMessage.sessionID, sessionGuid=slpMessage.sessionGuid)
+ slpLink.write(self.msnobj.imageData)
+ slpLink.close()
+ else:
+ # They shouldn't have sent a request if we have
+ # no avatar. So we'll just ignore them.
+ # FIXME We should really send an error
+ pass
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)
-
- return 1
-
- 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 _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))
- 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)
+ slpLink.sendP2PACK(binaryFields)
- 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)
def checkMessage(self, message):
"""
"""
cTypes = [s.lstrip() for s in message.getHeader('Content-Type').split(';')]
if self._checkTyping(message, cTypes): return 0
- if 'text/x-msmsgsinvite' in cTypes:
+# if 'text/x-msmsgsinvite' in cTypes:
# header like info is sent as part of the message body.
- info = {}
- for line in message.message.split('\r\n'):
- try:
- key, val = line.split(':')
- 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
+# info = {}
+# for line in message.message.split('\r\n'):
+# try:
+# key, val = line.split(':')
+# info[key] = val.lstrip()
+# except ValueError: continue
+# if self._checkFileInvitation(message, info): return 0
+ elif 'application/x-msnmsgrp2p' in cTypes:
+ self._handleP2PMessage(message)
+ return 0
return 1
# negotiation
"""
pass
- def gotAvatarImage(self, userHandle, image):
+ def gotFileReceive(self, fileReceive):
"""
- called when we receive an avatar from a contact
+ called when we receive a file send request from a contact.
+ Default action is to reject the file.
- @param userHandle: the person who's avatar we have got
- @param image: the avatar image
+ @param fileReceive: msnft.MSNFTReceive_Base instance
"""
- pass
+ fileReceive.reject()
+
def gotSendRequest(self, fileReceive):
"""
"""
pass
- def userTyping(self, message):
+ def gotContactTyping(self, message):
"""
called when we receive the special type of message notifying
- us that a user is typing a message.
+ us that a contact is typing a message.
@param message: the associated MSNMessage object
"""
else: id, d = self._createIDMapping()
if message.length == 0: message.length = message._calcMessageLen()
self.sendLine("MSG %s %s %s" % (id, message.ack, message.length))
- # apparently order matters with at least MIME-Version and Content-Type
- self.sendLine('MIME-Version: %s' % message.getHeader('MIME-Version'))
- self.sendLine('Content-Type: %s' % message.getHeader('Content-Type'))
+ # Apparently order matters with these
+ orderMatters = ("MIME-Version", "Content-Type", "Message-ID")
+ for header in orderMatters:
+ if message.hasHeader(header):
+ self.sendLine("%s: %s" % (header, message.getHeader(header)))
# send the rest of the headers
- for header in [h for h in message.headers.items() if h[0].lower() not in ('mime-version','content-type')]:
+ for header in [h for h in message.headers.items() if h[0] not in orderMatters]:
self.sendLine("%s: %s" % (header[0], header[1]))
- self.transport.write(CR+LF)
+ self.transport.write("\r\n")
self.transport.write(message.message)
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)
+ buffer.error = lambda: None
+ slpLink = SLPLink_AvatarReceive(remoteUser=msnContact.userHandle, switchboard=self, consumer=buffer, context=msnContact.msnobj.text)
+ self.slpLinks[slpLink.sessionID] = slpLink
+ return d
+
+ def sendFile(self, msnContact, filename, filesize):
+ """
+ used to send a file to a contact.
+
+ @param msnContact: the MSNContact object to send a file to.
+ @param filename: the name of the file to send.
+ @param filesize: the size of the file to send.
+
+ @return: (fileSend, d) A FileSend object and a Deferred.
+ The Deferred will pass one argument in a tuple,
+ whether or not the transfer is accepted. If you
+ receive a True, then you can call write() on the
+ fileSend object to send your file. Call close()
+ when the file is done.
+ NOTE: You MUST write() exactly as much as you
+ declare in filesize.
+ """
+ if not msnContact.userHandle: return
+ # FIXME, check msnContact.caps to see if we should use old-style
+ fileSend = SLPLink_FileSend(remoteUser=msnContact.userHandle, switchboard=self, filename=filename, filesize=filesize)
+ self.slpLinks[fileSend.sessionID] = fileSend
+ return fileSend, fileSend.acceptDeferred
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
m.ack = m.MESSAGE_NACK
self.sendMessage(m)
-class FileSend(LineReceiver):
- """
- This class provides support for sending files to other contacts.
-
- @ivar bytesSent: the number of bytes that have currently been sent.
- @ivar completed: true if the send has completed.
- @ivar connected: true if a connection has been established.
- @ivar targetUser: the target user (contact).
- @ivar segmentSize: the segment (block) size.
- @ivar auth: the auth cookie (number) to use when sending the
- transfer invitation
- """
+
+class FileReceive:
+ def __init__(self, filename, filesize, userHandle):
+ self.consumer = None
+ self.finished = False
+ self.error = False
+ self.buffer = []
+ self.filename, self.filesize, self.userHandle = filename, filesize, userHandle
+
+ def reject(self):
+ raise NotImplementedError
+
+ def accept(self, consumer):
+ if self.consumer: raise "AlreadyAccepted"
+ self.consumer = consumer
+ for data in self.buffer:
+ self.consumer.write(data)
+ self.buffer = None
+ if self.finished:
+ self.consumer.close()
+ if self.error:
+ self.consumer.error()
+
+ def write(self, data):
+ if self.error or self.finished:
+ raise IOError, "Attempt to write in an invalid state"
+ if self.consumer:
+ self.consumer.write(data)
+ else:
+ self.buffer.append(data)
+
+ def close(self):
+ self.finished = True
+ if self.consumer:
+ self.consumer.close()
+
+class FileContext:
+ """ Represents the Context field for P2P file transfers """
+ def __init__(self, data=""):
+ if data:
+ self.parse(data)
+ else:
+ self.filename = ""
+ self.filesize = 0
+
+ def pack(self):
+ if MSNP2PDEBUG: log.msg("FileContext packing:", self.filename, self.filesize)
+ data = struct.pack("<LLQL", 638, 0x03, self.filesize, 0x01)
+ data = data[:-1] # Uck, weird, but it works
+ data += utf16net(self.filename)
+ data = ljust(data, 570, '\0')
+ data += struct.pack("<L", 0xFFFFFFFFL)
+ data = ljust(data, 638, '\0')
+ return data
+
+ def parse(self, packet):
+ self.filesize = struct.unpack("<Q", packet[8:16])[0]
+ chunk = packet[19:540]
+ chunk = chunk[:chunk.find('\x00\x00')]
+ self.filename = unicode((codecs.BOM_UTF16_BE + chunk).decode("utf-16"))
+ if MSNP2PDEBUG: log.msg("FileContext parsed:", self.filesize, self.filename)
+
+
+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 __init__(self, file):
- """
- @param file: A string or file object represnting the file to send.
- """
+ 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:
+ out = "Unpacked fields: "
+ for i in self.fields:
+ out += hex(i) + ' '
+ log.msg(out)
+
+ def packHeaders(self):
+ f = tuple(self.fields)
+ if MSNP2PDEBUG:
+ out = "Packed fields: "
+ for i in self.fields:
+ out += hex(i) + ' '
+ log.msg(out)
+ 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.branch = ""
+ 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, branch=None, cseq=0, sessionGuid=None, data=None):
+ self.method = method
+ self.status = status
+ self.to = to
+ self.fro = fro
+ self.branch = branch
+ self.cseq = cseq
+ self.sessionGuid = sessionGuid
+ if data: self.data = data
+
+ def setData(self, ctype, data):
+ self.ctype = ctype
+ s = []
+ order = ["EUF-GUID", "SessionID", "AppID", "Context", "Bridge", "Listening","Bridges", "NetID", "Conn-Type", "UPnPNat", "ICF", "Hashed-Nonce"]
+ for key in order:
+ if key == "Context" and data.has_key(key):
+ s.append("Context: %s\r\n" % b64enc(data[key]))
+ elif data.has_key(key):
+ s.append("%s: %s\r\n" % (key, str(data[key])))
+ s.append("\r\n"+chr(0))
+
+ self.data = "".join(s)
+
+ def parse(self, s):
+ s = s[48:len(s)-4:]
+ if s.find("MSNSLP/1.0") < 0: return
- if isinstance(file, types.StringType):
- self.file = open(file, 'rb')
+ lines = s.split("\r\n")
+
+ # Get the MSNSLP method or status
+ msnslp = lines[0].split(" ")
+ if MSNP2PDEBUG: log.msg("Parsing MSNSLPMessage %s %s" % (len(s), s))
+ if msnslp[0] in ("INVITE", "BYE"):
+ self.method = msnslp[0].strip()
else:
- self.file = file
+ self.status = msnslp[1].strip()
- self.fileSize = 0
- self.bytesSent = 0
- self.completed = 0
- self.connected = 0
- self.targetUser = None
- self.segmentSize = 2045
- self.auth = random.randint(0, 2**30)
- self._pendingSend = None # :(
+ lines.remove(lines[0])
+
+ for line in lines:
+ line = line.split(":")
+ if len(line) < 1: continue
+ try:
+ 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()
+ elif line[0] == "Content-Type":
+ self.ctype = line[1].strip()
+ elif line[0] == "Context":
+ self.context = b64dec(line[1])
+ elif line[0] == "Via":
+ self.branch = line[1].split(";")[1].split("=")[1].strip()
+ except:
+ if MSNP2PDEBUG:
+ log.msg("Error parsing MSNSLP message.")
+ raise
+
+ def __str__(self):
+ s = []
+ if self.method:
+ s.append("%s MSNMSGR:%s MSNSLP/1.0\r\n" % (self.method, self.to))
+ else:
+ if self.status == "200": status = "200 OK"
+ elif self.status == "603": status = "603 Decline"
+ s.append("MSNSLP/1.0 %s\r\n" % 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" % self.branch)
+ 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")
+ s.append("Content-Type: %s\r\n" % self.ctype)
+ 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.baseID = baseID
+ else:
+ self.baseID = random.randint(1000, sys.maxint)
+ self.pos = -1
- def connectionMade(self):
- self.connected = 1
+ def get(self):
+ return p2pseq(self.pos) + self.baseID
+
+ def next(self):
+ self.pos += 1
+ return self.get()
+
- def connectionLost(self, reason):
- if self._pendingSend:
- self._pendingSend.cancel()
- self._pendingSend = None
- self.connected = 0
- self.file.close()
+class StringBuffer(StringIO.StringIO):
+ def __init__(self, notifyFunc=None):
+ self.notifyFunc = notifyFunc
+ StringIO.StringIO.__init__(self)
+
+ def close(self):
+ if self.notifyFunc:
+ self.notifyFunc(self.getvalue())
+ self.notifyFunc = None
+ StringIO.StringIO.close(self)
+
+
+class SLPLink:
+ def __init__(self, remoteUser, switchboard, sessionID, sessionGuid):
+ self.dataFlag = 0
+ 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 killLink(self):
+ if MSNP2PDEBUG: log.msg("killLink")
+ def kill():
+ if MSNP2PDEBUG: log.msg("killLink - kill()")
+ if not self.switchboard: return
+ del self.switchboard.slpLinks[self.sessionID]
+ self.switchboard = None
+ # This is so that handleP2PMessage can still use the SLPLink
+ # one last time, for ACKing BYEs and 601s.
+ reactor.callLater(0, kill)
+
+ def warn(self, text):
+ log.msg("Warning in transfer: %s %s" % (self, text))
+
+ def sendP2PACK(self, ackHeaders):
+ binaryFields = BinaryFields()
+ binaryFields[0] = ackHeaders[0]
+ binaryFields[1] = self.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, "")
- def lineReceived(self, line):
- temp = line.split(' ')
- if len(temp) == 1: params = []
- else: params = temp[1:]
- cmd = temp[0]
- handler = getattr(self, "handle_%s" % cmd.upper(), None)
- if handler: handler(params)
- else: self.handle_UNKNOWN(cmd, params)
+ def sendSLPMessage(self, cmd, ctype, data, branch=None):
+ msg = MSNSLPMessage()
+ if cmd.isdigit():
+ msg.create(status=cmd, to=self.remoteUser, fro=self.switchboard.userHandle, branch=branch, cseq=1, sessionGuid=self.sessionGuid)
+ else:
+ msg.create(method=cmd, to=self.remoteUser, fro=self.switchboard.userHandle, branch=random_guid(), cseq=0, sessionGuid=self.sessionGuid)
+ msg.setData(ctype, data)
+ msgStr = str(msg)
+ binaryFields = BinaryFields()
+ binaryFields[1] = self.seqID.next()
+ binaryFields[3] = len(msgStr)
+ binaryFields[4] = binaryFields[3]
+ binaryFields[6] = random.randint(1000, sys.maxint)
+ self.sendP2PMessage(binaryFields, msgStr)
- def handle_VER(self, params):
- checkParamLen(len(params), 1, 'VER')
- if params[0].upper() == "MSNFTP":
- self.sendLine("VER MSNFTP")
- else: # they sent some weird version during negotiation, i'm quitting.
- self.transport.loseConnection()
+ def sendP2PMessage(self, binaryFields, msgStr):
+ packet = binaryFields.packHeaders() + msgStr + binaryFields.packFooter()
- def handle_USR(self, params):
- checkParamLen(len(params), 2, 'USR')
- self.targetUser = params[0]
- if self.auth == int(params[1]):
- self.sendLine("FIL %s" % (self.fileSize))
- else: # they failed the auth test, disconnecting.
- self.transport.loseConnection()
+ message = MSNMessage(message=packet)
+ message.setHeader("Content-Type", "application/x-msnmsgrp2p")
+ message.setHeader("P2P-Dest", self.remoteUser)
+ message.ack = MSNMessage.MESSAGE_ACK_FAT
+ self.switchboard.sendMessage(message)
+
+ def handleSLPMessage(self, slpMessage):
+ raise NotImplementedError
- def handle_TFR(self, params):
- checkParamLen(len(params), 0, 'TFR')
- # they are ready for me to start sending
- self.sendPart()
- def handle_BYE(self, params):
- self.completed = (self.bytesSent == self.fileSize)
- self.transport.loseConnection()
- def handle_CCL(self, params):
- self.completed = (self.bytesSent == self.fileSize)
- self.transport.loseConnection()
+
+
+class SLPLink_Send(SLPLink):
+ def __init__(self, remoteUser, switchboard, filesize, sessionID=None, sessionGuid=None):
+ SLPLink.__init__(self, remoteUser, switchboard, sessionID, sessionGuid)
+ self.handlePacket = None
+ self.offset = 0
+ self.filesize = filesize
+ self.data = ""
+
+ def send_dataprep(self):
+ if MSNP2PDEBUG: log.msg("send_dataprep")
+ binaryFields = BinaryFields()
+ binaryFields[0] = self.sessionID
+ binaryFields[1] = self.seqID.next()
+ binaryFields[3] = 4
+ binaryFields[4] = 4
+ binaryFields[6] = random.randint(1000, sys.maxint)
+ binaryFields[9] = 1
+ self.sendP2PMessage(binaryFields, chr(0) * 4)
+
+ def write(self, data):
+ if MSNP2PDEBUG: log.msg("write")
+ i = 0
+ data = self.data + data
+ self.data = ""
+ length = len(data)
+ while i < length:
+ if i + 1202 < length:
+ self._writeChunk(data[i:i+1202])
+ i += 1202
+ else:
+ self.data = data[i:]
+ return
+
+ def _writeChunk(self, chunk):
+ if MSNP2PDEBUG: log.msg("writing chunk")
+ binaryFields = BinaryFields()
+ binaryFields[0] = self.sessionID
+ if self.offset == 0:
+ binaryFields[1] = self.seqID.next()
+ else:
+ binaryFields[1] = self.seqID.get()
+ binaryFields[2] = self.offset
+ binaryFields[3] = self.filesize
+ binaryFields[4] = len(chunk)
+ binaryFields[5] = self.dataFlag
+ binaryFields[6] = random.randint(1000, sys.maxint)
+ binaryFields[9] = 1
+ self.offset += len(chunk)
+ self.sendP2PMessage(binaryFields, chunk)
+
+ def close(self):
+ if self.data:
+ self._writeChunk(self.data)
+ #self.killLink()
+
+ def error(self):
+ pass
+ # FIXME, should send 601 or something
+
+class SLPLink_FileSend(SLPLink_Send):
+ def __init__(self, remoteUser, switchboard, filename, filesize):
+ SLPLink_Send.__init__(self, remoteUser=remoteUser, switchboard=switchboard, filesize=filesize)
+ self.dataFlag = BinaryFields.DATAFT
+ # Send invite & wait for 200OK before sending dataprep
+ context = FileContext()
+ context.filename = filename
+ context.filesize = filesize
+ data = {"EUF-GUID" : MSN_MSNFTP_GUID,\
+ "SessionID": self.sessionID,\
+ "AppID" : 2,\
+ "Context" : context.pack() }
+ self.sendSLPMessage("INVITE", "application/x-msnmsgr-sessionreqbody", data)
+ self.acceptDeferred = Deferred()
+
+ def handleSLPMessage(self, slpMessage):
+ if slpMessage.status == "200":
+ if slpMessage.ctype == "application/x-msnmsgr-sessionreqbody":
+ data = {"Bridges" : "TRUDPv1 TCPv1",\
+ "NetID" : "0",\
+ "Conn-Type" : "Firewall",\
+ "UPnPNat" : "false",\
+ "ICF" : "true",}
+ #"Hashed-Nonce": random_guid()}
+ self.sendSLPMessage("INVITE", "application/x-msnmsgr-transreqbody", data)
+ elif slpMessage.ctype == "application/x-msnmsgr-transrespbody":
+ self.acceptDeferred.callback((True,))
+ self.handlePacket = self.wait_data_ack
+ else:
+ if slpMessage.status == "603":
+ self.acceptDeferred.callback((False,))
+ if MSNP2PDEBUG: log.msg("SLPLink is over due to decline, error or BYE")
+ self.killLink()
+
+ def wait_data_ack(self, packet):
+ if MSNP2PDEBUG: log.msg("wait_data_ack")
+ binaryFields = BinaryFields()
+ binaryFields.unpackFields(packet)
+
+ if binaryFields[5] != BinaryFields.ACK:
+ self.warn("field5," + str(binaryFields[5]))
+ return
- def handle_UNKNOWN(self, cmd, params): log.msg('received unknown command (%s), params: %s' % (cmd, params))
+ self.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {})
+ self.handlePacket = None
- def makeHeader(self, size):
- """ make the appropriate header given a specific segment size. """
- quotient, remainder = divmod(size, 256)
- return chr(0) + chr(remainder) + chr(quotient)
+ def close(self):
+ self.handlePacket = self.wait_data_ack
+ SLPLink_Send.close(self)
- def sendPart(self):
- """ send a segment of data """
- if not self.connected:
- self._pendingSend = None
- return # may be buggy (if handle_CCL/BYE is called but self.connected is still 1)
- data = self.file.read(self.segmentSize)
- if data:
- dataSize = len(data)
- header = self.makeHeader(dataSize)
- self.transport.write(header + data)
- self.bytesSent += dataSize
- self._pendingSend = reactor.callLater(0, self.sendPart)
+
+class SLPLink_AvatarSend(SLPLink_Send):
+ def __init__(self, remoteUser, switchboard, filesize, sessionID=None, sessionGuid=None):
+ SLPLink_Send.__init__(self, remoteUser=remoteUser, switchboard=switchboard, filesize=filesize, sessionID=sessionID, sessionGuid=sessionGuid)
+ self.dataFlag = BinaryFields.DATA
+ self.sendSLPMessage("200", "application/x-msnmsgr-sessionreqbody", {"SessionID":self.sessionID})
+ self.send_dataprep()
+ self.handlePacket = lambda packet: None
+
+ def handleSLPMessage(self, slpMessage):
+ if MSNP2PDEBUG: log.msg("BYE or error")
+ self.killLink()
+
+ def close(self):
+ SLPLink_Send.close(self)
+ # Keep the link open to wait for a BYE
+
+class SLPLink_Receive(SLPLink):
+ def __init__(self, remoteUser, switchboard, consumer, context=None, sessionID=None, sessionGuid=None):
+ SLPLink.__init__(self, remoteUser, switchboard, sessionID, sessionGuid)
+ self.handlePacket = None
+ self.consumer = consumer
+ self.pos = 0
+
+ def wait_dataprep(self, packet):
+ if MSNP2PDEBUG: log.msg("wait_dataprep")
+ binaryFields = BinaryFields()
+ binaryFields.unpackFields(packet)
+
+ if binaryFields[3] != 4:
+ self.warn("field3," + str(binaryFields[3]))
+ return
+ if binaryFields[4] != 4:
+ self.warn("field4," + str(binaryFields[4]))
+ return
+ # Just ignore the footer
+ #if binaryFields[9] != 1:
+ # self.warn("field9," + str(binaryFields[9]))
+ # return
+
+ self.sendP2PACK(binaryFields)
+ self.handlePacket = self.wait_data
+
+ def wait_data(self, packet):
+ if MSNP2PDEBUG: log.msg("wait_data")
+ binaryFields = BinaryFields()
+ binaryFields.unpackFields(packet)
+
+ if binaryFields[5] != self.dataFlag:
+ self.warn("field5," + str(binaryFields[5]))
+ return
+ # Just ignore the footer
+ #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.warn("Received packet out of order")
+ self.consumer.error()
+ return
+ if len(data) != length:
+ self.warn("Received bad length of slp")
+ self.consumer.error()
+ return
+
+ self.pos += length
+
+ self.consumer.write(str(data))
+
+ if self.pos == total:
+ self.sendP2PACK(binaryFields)
+ self.consumer.close()
+ self.handlePacket = None
+ self.doFinished()
+
+ def doFinished(self):
+ raise NotImplementedError
+
+
+class SLPLink_FileReceive(SLPLink_Receive, FileReceive):
+ def __init__(self, remoteUser, switchboard, filename, filesize, sessionID, sessionGuid, branch):
+ SLPLink_Receive.__init__(self, remoteUser=remoteUser, switchboard=switchboard, consumer=self, sessionID=sessionID, sessionGuid=sessionGuid)
+ self.dataFlag = BinaryFields.DATAFT
+ self.initialBranch = branch
+ FileReceive.__init__(self, filename, filesize, remoteUser)
+
+ def reject(self):
+ # Send a 603 decline
+ if not self.switchboard: return
+ self.sendSLPMessage("603", "application/x-msnmsgr-sessionreqbody", {"SessionID":self.sessionID}, branch=self.initialBranch)
+ self.killLink()
+
+ def accept(self, consumer):
+ FileReceive.accept(self, consumer)
+ if not self.switchboard: return
+ self.sendSLPMessage("200", "application/x-msnmsgr-sessionreqbody", {"SessionID":self.sessionID}, branch=self.initialBranch)
+ self.handlePacket = self.wait_data # Moved here because sometimes the second INVITE seems to be skipped
+
+ def handleSLPMessage(self, slpMessage):
+ if slpMessage.method == "INVITE": # The second invite
+ data = {"Bridge" : "TCPv1",\
+ "Listening" : "false",\
+ "Hashed-Nonce": "{00000000-0000-0000-0000-000000000000}"}
+ self.sendSLPMessage("200", "application/x-msnmsgr-transrespbody", data, branch=slpMessage.branch)
+# self.handlePacket = self.wait_data # Moved up
+ else:
+ if MSNP2PDEBUG: log.msg("It's either a BYE or an error")
+ self.killLink()
+ # FIXME, do some error handling if it was an error
+
+ def doFinished(self):
+ #self.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {})
+ #self.killLink()
+ # Wait for BYE? #FIXME
+ pass
+
+class SLPLink_AvatarReceive(SLPLink_Receive):
+ def __init__(self, remoteUser, switchboard, consumer, context):
+ SLPLink_Receive.__init__(self, remoteUser=remoteUser, switchboard=switchboard, consumer=consumer, context=context)
+ self.dataFlag = BinaryFields.DATA
+ data = {"EUF-GUID" : MSN_AVATAR_GUID,\
+ "SessionID": self.sessionID,\
+ "AppID" : 1,\
+ "Context" : context}
+ self.sendSLPMessage("INVITE", "application/x-msnmsgr-sessionreqbody", data)
+ self.handlePacket = self.wait_dataprep
+
+ def handleSLPMessage(self, slpMessage):
+ if slpMessage.status == "200":
+ pass
+ #self.handlePacket = self.wait_dataprep # Moved upwards
else:
- self._pendingSend = None
- self.completed = 1
+ if MSNP2PDEBUG: log.msg("SLPLink is over due to error or BYE")
+ self.killLink()
+
+ def doFinished(self):
+ self.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {})
# mapping of error codes to error messages
errorCodes = {
301 : "Too many FND responses",
302 : "Not logged in",
+ 400 : "Message not allowed",
402 : "Error accessing contact list",
403 : "Error accessing contact list",