X-Git-Url: https://code.delx.au/pymsnt/blobdiff_plain/c95778ec00086d02220260b8ebc67dd23ce06182..6faa958d34a40175b4f831e53ed96a01df35d8da:/src/tlib/msn/msn.py diff --git a/src/tlib/msn/msn.py b/src/tlib/msn/msn.py index 02adcdd..fcab164 100644 --- a/src/tlib/msn/msn.py +++ b/src/tlib/msn/msn.py @@ -99,16 +99,19 @@ import msnp11chl # 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, struct, random, sha, base64, StringIO +import types, operator, os, sys, base64, random, struct, random, sha, base64, StringIO, array, codecs, binascii from urllib import quote, unquote @@ -149,11 +152,9 @@ STATUS_BRB = 'BRB' STATUS_PHONE = 'PHN' STATUS_LUNCH = 'LUN' -CR = "\r" -LF = "\n" PINGSPEED = 50.0 -DEBUGALL = False +DEBUGALL = True LINEDEBUG = False MESSAGEDEBUG = False MSNP2PDEBUG = False @@ -163,6 +164,15 @@ if DEBUGALL: 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] @@ -191,9 +201,32 @@ def getVals(params): 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 = [] @@ -313,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 = {} @@ -400,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 @@ -411,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 @@ -748,13 +786,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()) @@ -773,8 +812,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 @@ -789,6 +830,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 @@ -804,9 +846,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) @@ -836,16 +882,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)) @@ -858,10 +901,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" @@ -887,6 +930,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 @@ -922,6 +977,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') @@ -1001,16 +1057,17 @@ 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(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): @@ -1090,7 +1147,7 @@ class NotificationClient(MSNEventBase): 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: @@ -1104,7 +1161,7 @@ class NotificationClient(MSNEventBase): 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') @@ -1126,7 +1183,7 @@ class NotificationClient(MSNEventBase): 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') @@ -1206,18 +1263,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) @@ -1245,7 +1302,7 @@ 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 < 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() @@ -1265,7 +1322,7 @@ 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 < 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() @@ -1324,7 +1381,7 @@ class NotificationClient(MSNEventBase): 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') @@ -1334,6 +1391,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 @@ -1406,7 +1464,7 @@ class NotificationClient(MSNEventBase): 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): @@ -1418,22 +1476,22 @@ class NotificationClient(MSNEventBase): """ 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 @@ -1792,6 +1850,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 @@ -1858,14 +1917,13 @@ class NotificationClient(MSNEventBase): @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) @@ -1939,9 +1997,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 @@ -1964,6 +2024,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 @@ -1971,8 +2033,9 @@ class NotificationFactory(ClientFactory): screenName = '' password = '' passportServer = 'https://nexus.passport.com/rdr/pprdr.asp' - status = 'FLN' + status = 'NLN' protocol = NotificationClient + maxRetries = 5 class SwitchboardClient(MSNEventBase): @@ -1995,7 +2058,8 @@ class SwitchboardClient(MSNEventBase): to a switchboard invitation @ivar reply: set this to 1 in connectionMade or before to signifiy that you are replying to a switchboard invitation. - @ivar msnobj: the MSNObject for the user. So that the switchboard can distribute it. + @ivar msnobj: the MSNObject for the user's avatar. So that the + switchboard can distribute it to anyone who asks. """ key = 0 @@ -2039,7 +2103,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): @@ -2049,32 +2113,47 @@ 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 _handleP2PMessage(self, message): """ helper method for msnslp messages (file transfer & avatars) """ + if not message.getHeader("P2P-Dest") == self.userHandle: return packet = message.message binaryFields = BinaryFields(packet=packet) - if binaryFields[0] != 0: - slpLink = self.slpLinks[binaryFields[0]] + 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 = 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: - #FIXME! context = FileContext(slpMessage.context) - slpLink = SLPLink_FileReceive(remoteUser=slpMessage.fro, switchboard=self, filename=context.filename, filesize=context.filesize, sessionID=slpMessage.sessionID, sessionGuid=slpMessage.sessionGuid) + 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: @@ -2086,13 +2165,10 @@ class SwitchboardClient(MSNEventBase): 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: - for slpLink in self.slpLinks.values(): - if slpLink.sessionGuid == slpMessage.sessionGuid: - slpLink.handleSLPMessage(slpMessage) if slpLink: # Always need to ACK these packets if we can slpLink.sendP2PACK(binaryFields) @@ -2105,15 +2181,15 @@ 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 +# 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 @@ -2208,11 +2284,12 @@ class SwitchboardClient(MSNEventBase): def gotFileReceive(self, fileReceive): """ - called when we receive a file send request from a contact + called when we receive a file send request from a contact. + Default action is to reject the file. @param fileReceive: msnft.MSNFTReceive_Base instance """ - pass + fileReceive.reject() def gotSendRequest(self, fileReceive): @@ -2223,10 +2300,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 """ @@ -2270,13 +2347,15 @@ 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 @@ -2298,8 +2377,8 @@ class SwitchboardClient(MSNEventBase): 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) - slpLink.avatarDataBuffer = buffer self.slpLinks[slpLink.sessionID] = slpLink return d @@ -2445,19 +2524,21 @@ class FileContext: self.filesize = 0 def pack(self): - if MSNP2PDEBUG: print "FileContext packing:", self.filename, self.filesize - data = struct.pack("L", packet[len(packet)-4:]) if MSNP2PDEBUG: - print "Unpacked fields:", + out = "Unpacked fields: " for i in self.fields: - print hex(i), - print + out += hex(i) + ' ' + log.msg(out) def packHeaders(self): f = tuple(self.fields) if MSNP2PDEBUG: - print "Packed fields:", + out = "Packed fields: " for i in self.fields: - print hex(i), - print + out += hex(i) + ' ' + log.msg(out) return struct.pack(" 1: + 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": @@ -2571,50 +2658,52 @@ class MSNSLPMessage: 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 = base64.decodestring(line[1]) + 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)) + s.append("%s MSNMSGR:%s MSNSLP/1.0\r\n" % (self.method, self.to)) else: - s.append("MSNSLP/1.0 %s\r\n" % self.status) + 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" % random_guid()) + 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") - if self.method == "BYE": - s.append("Content-Type: application/x-msnmsgr-sessionclosebody\r\n") - else: - s.append("Content-Type: application/x-msnmsgr-sessionreqbody\r\n") + s.append("Content-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.seqID = baseID + self.baseID = baseID else: - self.seqID = random.randint(1000, sys.maxint) + self.baseID = random.randint(1000, sys.maxint) self.pos = -1 - + def get(self): - if self.pos == 0: - return self.seqID - else: - return self.seqID + self.pos - 3 + return p2pseq(self.pos) + self.baseID def next(self): self.pos += 1 - if self.pos == 3: - self.pos += 1 return self.get() + class StringBuffer(StringIO.StringIO): def __init__(self, notifyFunc=None): @@ -2630,6 +2719,7 @@ class StringBuffer(StringIO.StringIO): class SLPLink: def __init__(self, remoteUser, switchboard, sessionID, sessionGuid): + self.dataFlag = 0 if not sessionID: sessionID = random.randint(1000, sys.maxint) if not sessionGuid: @@ -2641,23 +2731,22 @@ class SLPLink: 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 setError(self, text): - if MSNP2PDEBUG: - print "ERROR in avatar transfer: ", self, text, "in state:", self.state - def warn(self, text): - if MSNP2PDEBUG: - print "Warning in avatar transfer: ", self, text, "in state:", self.state + 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 @@ -2666,20 +2755,14 @@ class SLPLink: binaryFields[8] = ackHeaders[3] self.sendP2PMessage(binaryFields, "") - def sendMSNSLPCommand(self, command, guid, context): - msg = MSNSLPMessage() - msg.create(method=command, to=self.remoteUser, fro=self.switchboard.userHandle, cseq=0, sessionGuid=self.sessionGuid) - msg.setData(sessionID=self.sessionID, appID="1", guid=guid, context=b64enc(context + chr(0))) - self.sendMSNSLPMessage(msg) - - def sendMSNSLPResponse(self, response): + def sendSLPMessage(self, cmd, ctype, data, branch=None): msg = MSNSLPMessage() - msg.create(status=response, to=self.remoteUser, fro=self.switchboard.userHandle, cseq=1, sessionGuid=self.sessionGuid) - msg.setData(sessionID=self.sessionID) - self.sendMSNSLPMessage(msg) - - def sendMSNSLPMessage(self, msnSLPMessage): - msgStr = str(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) @@ -2696,7 +2779,7 @@ class SLPLink: message.ack = MSNMessage.MESSAGE_ACK_FAT self.switchboard.sendMessage(message) - def handleSLPMessage(self): + def handleSLPMessage(self, slpMessage): raise NotImplementedError @@ -2712,6 +2795,7 @@ class SLPLink_Send(SLPLink): self.data = "" def send_dataprep(self): + if MSNP2PDEBUG: log.msg("send_dataprep") binaryFields = BinaryFields() binaryFields[0] = self.sessionID binaryFields[1] = self.seqID.next() @@ -2722,28 +2806,31 @@ class SLPLink_Send(SLPLink): 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:] - if len(self.data) >= 1202: - data = self.data - self.data = "" - self.write(data) + self.data = data[i:] return def _writeChunk(self, chunk): + if MSNP2PDEBUG: log.msg("writing chunk") binaryFields = BinaryFields() binaryFields[0] = self.sessionID - binaryFields[1] = self.seqID.get() + 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] = BinaryFields.DATA + binaryFields[5] = self.dataFlag binaryFields[6] = random.randint(1000, sys.maxint) binaryFields[9] = 1 self.offset += len(chunk) @@ -2752,7 +2839,7 @@ class SLPLink_Send(SLPLink): def close(self): if self.data: self._writeChunk(self.data) - self.killLink() + #self.killLink() def error(self): pass @@ -2761,34 +2848,65 @@ class SLPLink_Send(SLPLink): 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 - self.sendMSNSLPCommand("INVITE", MSN_MSNFTP_GUID, context.pack()) + 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": - self.send_dataprep() - self.acceptDeferred.callback((True,)) + 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,)) - # SLPLink is over due to decline, error or BYE + 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 + + self.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {}) + self.handlePacket = None + def close(self): + self.handlePacket = self.wait_data_ack SLPLink_Send.close(self) - # FIXME, check whether we should wait for a BYE or send one - self.sendMSNSLPCommand("BYE", MSN_AVATAR_GUID, "\0") 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.sendMSNSLPResponse("200 OK") + 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) @@ -2802,36 +2920,35 @@ class SLPLink_Receive(SLPLink): self.pos = 0 def wait_dataprep(self, packet): + if MSNP2PDEBUG: log.msg("wait_dataprep") binaryFields = BinaryFields() binaryFields.unpackFields(packet) - if binaryFields[0] != self.sessionID: - self.warn("field0," + str(binaryFields[0]) + "," + str(self.sessionID)) - return if binaryFields[3] != 4: - self.setError("field3," + str(binaryFields[3])) + self.warn("field3," + str(binaryFields[3])) return if binaryFields[4] != 4: - self.setError("field4," + str(binaryFields[4])) + self.warn("field4," + str(binaryFields[4])) return - if binaryFields[9] != 1: - self.warn("field9," + str(binaryFields[9])) + # 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[0] != self.sessionID: - self.warn("field0," + str(binaryFields[0]) + "," + str(self.sessionID)) - return - if binaryFields[5] != BinaryFields.DATA: - self.setError("field5," + str(binaryFields[5])) + + if binaryFields[5] != self.dataFlag: + self.warn("field5," + str(binaryFields[5])) return - if binaryFields[9] != 1: - self.warn("field9," + str(binaryFields[9])) + # Just ignore the footer + #if binaryFields[9] != 1: + # self.warn("field9," + str(binaryFields[9])) # return offset = binaryFields[2] total = binaryFields[3] @@ -2839,17 +2956,17 @@ class SLPLink_Receive(SLPLink): data = packet[48:-4] if offset != self.pos: - self.setError("Received packet out of order") + self.warn("Received packet out of order") self.consumer.error() return if len(data) != length: - self.setError("Received bad length of slp") + self.warn("Received bad length of slp") self.consumer.error() return self.pos += length - self.consumer.write(data) + self.consumer.write(str(data)) if self.pos == total: self.sendP2PACK(binaryFields) @@ -2862,39 +2979,63 @@ class SLPLink_Receive(SLPLink): class SLPLink_FileReceive(SLPLink_Receive, FileReceive): - def __init__(self, remoteUser, switchboard, filename, filesize, sessionID, sessionGuid): + 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): - pass + # 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) - self.sendMSNSLPResponse("200 OK") - self.handlePacket = self.wait_dataprep + 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): - # FIXME, do some error handling if it was an error - self.killLink() # It's either a BYE or an error + 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): - pass # Link is kept around waiting for a BYE + #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.sendMSNSLPCommand("INVITE", MSN_AVATAR_GUID, 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": - self.handlePacket = self.wait_dataprep + pass + #self.handlePacket = self.wait_dataprep # Moved upwards else: - # SLPLink is over due to error or BYE + if MSNP2PDEBUG: log.msg("SLPLink is over due to error or BYE") self.killLink() def doFinished(self): - self.sendMSNSLPCommand("BYE", MSN_AVATAR_GUID, "\0") + self.sendSLPMessage("BYE", "application/x-msnmsgr-sessionclosebody", {}) # mapping of error codes to error messages errorCodes = { @@ -2925,6 +3066,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",