X-Git-Url: https://code.delx.au/pymsnt/blobdiff_plain/ab0df99054306f0c734930d6d26ab0f49581ba4c..89a1109f8981cd7ff33f9ce2d974b712c9c4bb6c:/src/tlib/msn/msn.py diff --git a/src/tlib/msn/msn.py b/src/tlib/msn/msn.py index 62d645b..ac4cb55 100644 --- a/src/tlib/msn/msn.py +++ b/src/tlib/msn/msn.py @@ -78,13 +78,8 @@ main clients) is called, or an MSNProtocolError is raised, it's 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} +@author: U{James Bunton} """ from __future__ import nested_scopes @@ -100,24 +95,26 @@ 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 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 @@ -155,16 +152,90 @@ STATUS_BRB = 'BRB' STATUS_PHONE = 'PHN' STATUS_LUNCH = 'LUN' -CR = "\r" -LF = "\n" PINGSPEED = 50.0 -LINEDEBUG = True -MESSAGEDEBUG = True +DEBUGALL = False +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] +def getVals(params): + userHandle = "" + screenName = "" + userGuid = "" + lists = -1 + groups = [] + for p in params: + if not p: + continue + elif p[0] == 'N': + userHandle = getVal(p) + elif p[0] == 'F': + screenName = unquote(getVal(p)) + elif p[0] == 'C': + userGuid = getVal(p) + elif p.isdigit(): + lists = int(p) + else: # Must be the groups + try: + groups = p.split(',') + except: + raise MSNProtocolError, "Unknown LST/ADC response" + str(params) # debug + + 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 @@ -275,7 +346,7 @@ class PassportLogin(HTTPClient): 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 = {} @@ -362,7 +433,7 @@ class MSNMessage: 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 @@ -373,6 +444,11 @@ class MSNMessage: """ 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 @@ -396,6 +472,75 @@ 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. """ + self.setNull() + if s: + self.parse(s) + + 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 = '' % (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: """ @@ -409,7 +554,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 . @@ -640,13 +785,14 @@ class MSNEventBase(LineReceiver): 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()) @@ -665,8 +811,10 @@ class MSNEventBase(LineReceiver): 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 @@ -681,6 +829,7 @@ class MSNEventBase(LineReceiver): 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 @@ -696,9 +845,13 @@ class MSNEventBase(LineReceiver): 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) @@ -728,16 +881,13 @@ class MSNEventBase(LineReceiver): """ 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)) @@ -750,10 +900,10 @@ class DispatchClient(MSNEventBase): 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" @@ -779,6 +929,18 @@ class DispatchClient(MSNEventBase): 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 @@ -793,7 +955,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 @@ -814,6 +976,7 @@ class NotificationClient(MSNEventBase): 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') @@ -893,15 +1056,18 @@ class NotificationClient(MSNEventBase): 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("") + 5 p2 = lm.find("") if p1 >= 0 and p2 >= 0: - personal = unescapeFromXml(message.message[p1:p2]) - msnContact = self.factory.contacts.getContact(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): """ hook used for detecting specific notification messages """ @@ -966,7 +1132,7 @@ class NotificationClient(MSNEventBase): def handle_CHG(self, params): id = int(params[0]) if not self._fireCallback(id, params[1]): - self.factory.status = params[1] + if self.factory: self.factory.status = params[1] self.statusChanged(params[1]) def handle_ILN(self, params): @@ -976,18 +1142,25 @@ class NotificationClient(MSNEventBase): msnContact.status = params[1] msnContact.screenName = unquote(params[3]) if len(params) > 4: msnContact.caps = int(params[4]) - if len(params) > 5: self.handleAvatarHelper(msnContact, params[5]) - self.gotContactStatus(params[1], params[2], unquote(params[3])) + if len(params) > 5: + self.handleAvatarHelper(msnContact, params[5]) + else: + self.handleAvatarGoneHelper(msnContact) + self.gotContactStatus(params[2], params[1], unquote(params[3])) + + def handleAvatarGoneHelper(self, msnContact): + if msnContact.msnobj: + msnContact.msnobj = None + msnContact.msnobjGot = True + self.contactAvatarChanged(msnContact.userHandle, "") def handleAvatarHelper(self, msnContact, msnobjStr): - s = unquote(msnobjStr) - msnobj = msnp2p.MSNOBJ() - msnobj.parse(s) + 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.avatarHashChanged(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') @@ -1005,8 +1178,11 @@ class NotificationClient(MSNEventBase): msnContact.status = params[0] msnContact.screenName = unquote(params[2]) if len(params) > 3: msnContact.caps = int(params[3]) - if len(params) > 4: self.handleAvatarHelper(msnContact, params[4]) - self.contactStatusChanged(params[0], params[1], unquote(params[2])) + if len(params) > 4: + self.handleAvatarHelper(msnContact, params[4]) + else: + self.handleAvatarGoneHelper(msnContact) + self.contactStatusChanged(params[1], params[0], unquote(params[2])) def handle_FLN(self, params): checkParamLen(len(params), 1, 'FLN') @@ -1017,25 +1193,8 @@ class NotificationClient(MSNEventBase): def handle_LST(self, params): if self._getState() != 'SYNC': return - userHandle = "" - screenName = "" - userGuid = "" - lists = -1 - groups = [] - for p in params: - if p[0] == 'N': - userHandle = getVal(p) - elif p[0] == 'F': - screenName = unquote(getVal(p)) - elif p[0] == 'C': - userGuid = getVal(p) - elif p.isdigit(): - lists = int(p) - else: # Must be the groups - try: - groups = p.split(',') - except: - raise MSNProtocolError, "Unknown LST " + str(params) # debug + + userHandle, screenName, userGuid, lists, groups = getVals(params) if not userHandle or lists < 1: raise MSNProtocolError, "Unknown LST " + str(params) # debug @@ -1069,7 +1228,8 @@ class NotificationClient(MSNEventBase): self._getStateData('list').privacy = listCodeToID[params[0].lower()] else: id = int(params[0]) - self._fireCallback(id, listCodeToID[params[1].lower()]) + self.factory.contacts.privacy = listCodeToID[params[1].lower()] + self._fireCallback(id, params[1]) def handle_GTC(self, params): # check to see if this is in response to a SYN @@ -1102,18 +1262,18 @@ class NotificationClient(MSNEventBase): 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) @@ -1141,17 +1301,15 @@ class NotificationClient(MSNEventBase): def handle_ADC(self, params): if not self.factory.contacts: raise MSNProtocolError, "handle_ADC called with no contact list" numParams = len(params) - if numParams < 4 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() - userHandle = getVal(params[2]) - screenName = unquote(getVal(params[3])) - userGuid = None - if len(params) >= 5: userGuid = getVal(params[4]) -# if numParams == 6: # they sent a group id -# if params[1].upper() != "FL": raise MSNProtocolError, "Only forward list can contain groups" # debug -# groupID = int(params[5]) + userHandle, screenName, userGuid, ignored1, groups = getVals(params[2:]) + + if groups and listType.upper() != FORWARD_LIST: + raise MSNProtocolError, "Only forward list can contain groups" # debug + if not self._fireCallback(id, listCodeToID[listType], userGuid, userHandle, screenName): c = self.factory.contacts.getContact(userHandle) if not c: @@ -1163,15 +1321,15 @@ class NotificationClient(MSNEventBase): 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 < 4 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() - userHandle = params[3] + userHandle = params[2] groupID = None - if numParams == 5: + if numParams == 4: if params[1] != "FL": raise MSNProtocolError, "Only forward list can contain groups" # debug - groupID = int(params[4]) + groupID = int(params[3]) if not self._fireCallback(id, listCodeToID[listType], userHandle, groupID): if listType.upper() != "RL": return c = self.factory.contacts.getContact(userHandle) @@ -1218,8 +1376,11 @@ class NotificationClient(MSNEventBase): try: messageLen = int(params[1]) except ValueError: raise MSNProtocolError, "Invalid Parameter for UBX length argument" - self.currentMessage = MSNMessage(length=messageLen, userHandle=params[0], screenName="UBX", specialMessage=True) - self.setRawMode() + if messageLen > 0: + self.currentMessage = MSNMessage(length=messageLen, userHandle=params[0], screenName="UBX", specialMessage=True) + self.setRawMode() + else: + self._gotUBX(MSNMessage(userHandle=params[0])) def handle_UUX(self, params): checkParamLen(len(params), 2, 'UUX') @@ -1229,6 +1390,7 @@ class NotificationClient(MSNEventBase): 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 @@ -1295,40 +1457,40 @@ class NotificationClient(MSNEventBase): """ pass - def avatarHashChanged(self, userHandle, hash): + def contactAvatarChanged(self, userHandle, hash): """ Called when we receive the first, or a new from a 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): """ - Called when our status changes and it isn't in response to - a client command. + Called when our status changes and its not in response to a + client command. @param statusCode: 3-letter status code """ 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 @@ -1473,7 +1635,7 @@ class NotificationClient(MSNEventBase): id, d = self._createIDMapping() self.sendLine("CHG %s %s %s %s" % (id, status, str(MSNContact.MSNC1 | MSNContact.MSNC2 | MSNContact.MSNC3 | MSNContact.MSNC4), quote(self.msnobj.text))) def _cb(r): - if self.factory: self.factory.status = r[0] + self.factory.status = r[0] return r return d.addCallback(_cb) @@ -1498,9 +1660,8 @@ class NotificationClient(MSNEventBase): @return: A Deferred, the callback of which will be fired when the server replies with the new privacy setting. - The callback argument will be a tuple, the 2 elements - of which being the list version and either 'al' - or 'bl' (the new privacy setting). + The callback argument will be a tuple, the only element + of which being either 'al' or 'bl' (the new privacy setting). """ id, d = self._createIDMapping() @@ -1648,7 +1809,7 @@ class NotificationClient(MSNEventBase): return r return d.addCallback(_cb) - def addContact(self, listType, userHandle, groupID=""): + def addContact(self, listType, userHandle): """ Used to add a contact to the desired list. A default callback is added to the returned @@ -1664,9 +1825,6 @@ class NotificationClient(MSNEventBase): @param listType: (as defined by the *_LIST constants) @param userHandle: the user handle (passport) of the contact that is being added - @param groupID: the group ID for which to associate this contact - with. (default 0 - default group). Groups are only - valid for FORWARD_LIST. @return: A Deferred, the callback for which will be called when the server has clarified that the user has been added. @@ -1682,7 +1840,7 @@ class NotificationClient(MSNEventBase): except AttributeError: pass listType = listIDToCode[listType].upper() if listType == "FL": - self.sendLine("ADC %s %s N=%s F=%s %s" % (id, listType, userHandle, userHandle, groupID)) + self.sendLine("ADC %s %s N=%s F=%s" % (id, listType, userHandle, userHandle)) else: self.sendLine("ADC %s %s N=%s" % (id, listType, userHandle)) @@ -1691,6 +1849,7 @@ class NotificationClient(MSNEventBase): 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 @@ -1709,10 +1868,9 @@ class NotificationClient(MSNEventBase): @return: A Deferred, the callback for which will be called when the server has clarified that the user has been removed. - The callback argument will be a tuple of 4 elements: - the list type, the contact's user handle, the new list - version, and the group id (if relevant, otherwise it will - be None) + The callback argument will be a tuple of 3 elements: + the list type, the contact's user handle and the group ID + (if relevant, otherwise it will be None) """ id, d = self._createIDMapping() @@ -1730,11 +1888,12 @@ class NotificationClient(MSNEventBase): self.sendLine("REM %s %s %s" % (id, listType, userHandle)) def _cb(r): + if listType == "FL": + r = (r[0], userHandle, r[2]) # make sure we always get a userHandle l = self.factory.contacts - l.version = r[2] c = l.getContact(r[1]) if not c: return - group = r[3] + group = r[2] shouldRemove = 1 if group: # they may not have been removed from the list c.groups.remove(group) @@ -1756,19 +1915,29 @@ class NotificationClient(MSNEventBase): @param newName: the new screen name @return: A Deferred, the callback for which will be called - when the server sends an adequate reply. - The callback argument will be a tuple of 2 elements: - the new list version and the new screen name. + when the server acknowledges the change. + 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) def changePersonalMessage(self, personal): + """ + Used to change your personal message. + + @param personal: the new screen name + + @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 personal message. + """ + id, d = self._createIDMapping() data = "" if personal: @@ -1777,15 +1946,29 @@ class NotificationClient(MSNEventBase): self.transport.write(data) def _cb(r): self.factory.personal = personal + return (personal,) return d.addCallback(_cb) def changeAvatar(self, imageData, push): + """ + Used to change the avatar that other users see. + + @param imageData: the PNG image data to set as the avatar + @param push : whether to push the update to the server + (it will otherwise be sent with the next + changeStatus()) + + @return: If push==True, a Deferred, the callback for which + will be called when the server acknowledges the change. + The callback argument will be the same as for changeStatus. + """ + if self.msnobj and imageData == self.msnobj.imageData: return if imageData: self.msnobj.setData(self.factory.userHandle, imageData) else: self.msnobj.setNull() - if push: self.changeStatus(self.factory.status) # Push to server + if push: return self.changeStatus(self.factory.status) # Push to server def requestSwitchboardServer(self): @@ -1813,9 +1996,11 @@ class NotificationClient(MSNEventBase): 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 @@ -1838,6 +2023,8 @@ class NotificationFactory(ClientFactory): (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 @@ -1845,15 +2032,11 @@ class NotificationFactory(ClientFactory): 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. @@ -1874,21 +2057,23 @@ 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'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) @@ -1917,7 +2102,7 @@ class SwitchboardClient(MSNEventBase): 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): @@ -1927,93 +2112,66 @@ class SwitchboardClient(MSNEventBase): 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): """ @@ -2022,17 +2180,18 @@ class SwitchboardClient(MSNEventBase): """ 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 @@ -2122,14 +2281,15 @@ class SwitchboardClient(MSNEventBase): """ 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): """ @@ -2139,10 +2299,10 @@ class SwitchboardClient(MSNEventBase): """ 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 """ @@ -2186,27 +2346,71 @@ class SwitchboardClient(MSNEventBase): 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 @@ -2273,107 +2477,570 @@ class SwitchboardClient(MSNEventBase): 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("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("L", self.fields[9]) - def __init__(self, file): - """ - @param file: A string or file object represnting the file to send. - """ - if isinstance(file, types.StringType): - self.file = open(file, 'rb') +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 + + 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: \r\n" % self.to) + s.append("From: \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 handle_TFR(self, params): - checkParamLen(len(params), 0, 'TFR') - # they are ready for me to start sending - self.sendPart() + def handleSLPMessage(self, slpMessage): + raise NotImplementedError - 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() - def handle_UNKNOWN(self, cmd, params): log.msg('received unknown command (%s), params: %s' % (cmd, params)) + - 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) +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 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) + 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: - self._pendingSend = None - self.completed = 1 + 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.data = "" + 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 + + self.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {}) + self.handlePacket = None + + def close(self): + self.handlePacket = self.wait_data_ack + SLPLink_Send.close(self) + + +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.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) + elif slpMessage.status == "200": + pass + #self.handlePacket = self.wait_dataprep # Moved upwards + else: + 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 = { @@ -2404,6 +3071,7 @@ errorCodes = { 301 : "Too many FND responses", 302 : "Not logged in", + 400 : "Message not allowed", 402 : "Error accessing contact list", 403 : "Error accessing contact list",