]> code.delx.au - pymsnt/commitdiff
Initial file transfer work
authorjamesbunton <jamesbunton@55fbd22a-6204-0410-b2f0-b6c764c7e90a>
Mon, 31 Oct 2005 01:16:40 +0000 (01:16 +0000)
committerjamesbunton <jamesbunton@55fbd22a-6204-0410-b2f0-b6c764c7e90a>
Mon, 31 Oct 2005 01:16:40 +0000 (01:16 +0000)
git-svn-id: http://delx.cjb.net/svn/pymsnt/trunk@6 55fbd22a-6204-0410-b2f0-b6c764c7e90a

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

PyMSNt
config-example.xml
src/config.py
src/disco.py
src/ft.py [new file with mode: 0644]
src/legacy/glue.py
src/legacy/msnw.py
src/tlib/msn.py
src/tlib/msnft.py [new file with mode: 0644]

diff --git a/PyMSNt b/PyMSNt
index d73cc9d74382a9f1b819766ee4feb83af44c9f51..c341c1ff607a3a7c720de7a08bd509adcf1c688d 100755 (executable)
--- a/PyMSNt
+++ b/PyMSNt
@@ -1,6 +1,6 @@
 #!/bin/bash
 
-exec -a PyMSNt python src/main.py $*
+exec -a PyMSNt python main.py $*
 
 # Comment out the above line and use the below for twistd
 #PPATH="/usr/local/PyMSNt"
index 31b7b5f20f29be2653eaabdf5106f4930aa7291f..1175a0da03e0bf0efcfcc785384d8b09d6400849 100644 (file)
@@ -4,6 +4,8 @@
 
 <!-- The JabberID of the transport -->
 <jid>msn</jid>
+<!-- The public IP of the machine the transport is running on -->
+<ip>129.129.129.129</ip>
 <!-- The component JID of the transport. Unless you're doing clustering, leave this alone -->
 <!-- <compjid>msn1</compjid> -->
 
@@ -43,6 +45,16 @@ Do not include the jid of the transport -->
 <!-- Use Jabber.com's XCP component protocol extensions. --> 
 <!-- <useXCP/> -->
 
+
+<!-- File transfer settings -->
+<!-- The lowest and highest ports to give to MSN clients when they are sending files to us. These should be open in the firewall -->
+<ftLowPort>6891</ftLowPort>
+<ftHighPort>6899</ftHighPort>
+<!-- Please give the port to listen for HTTP GETs here (Used in OOB file transfers). -->
+<ftOOBPort>8080</ftOOBPort>
+<!-- Please give the root URL the transport should send to clients. (You can use an Apache reverse proxy to put this on your ordinary website) -->
+<ftOOBRoot>http://jabber.org/msn/files/</ftOOBRoot>
+
 <!-- You can choose which users you wish to have as administrators. These users can perform some tasks with Ad-Hoc commands that others cannot -->
 <!--<admins>
 <jid>admin@host.com</jid>
index 61d4e7149b1a6b62bc98aed288d9719536e209bf..6ab37fe6890555cf11a0f6007ec0388d6a7cc235 100644 (file)
@@ -2,6 +2,7 @@
 # Please edit config.xml instead of this file
 
 jid = "msn"
+ip = "127.0.0.1"
 compjid = ""
 spooldir = ""
 
@@ -20,5 +21,10 @@ allowRegister = False
 getAllAvatars = False
 useXCP = False
 
+ftLowPort = "6891"
+ftHighPort = "6899"
+ftOOBPort = ""
+ftOOBRoot = "http://" + ip + "/"
+
 admins = []
 
index a498fc7c027c9a8ef238b34c5c4dc42250468db3..fd8a0b986912c54158cb4a66ce8f2f1c66ca3a8b 100644 (file)
@@ -25,11 +25,17 @@ CAPS          = "http://jabber.org/protocol/caps"
 SUBSYNC       = "http://jabber.org/protocol/roster-subsync"
 MUC           = "http://jabber.org/protocol/muc"
 MUC_USER      = MUC + "#user"
+SI            = "http://jabber.org/protocol/si"
+FT            = "http://jabber.org/protocol/si/profile/file-transfer"
+S5B           = "http://jabber.org/protocol/bytestreams"
+IBB           = "http://jabber.org/protocol/ibb"
 IQGATEWAY     = "jabber:iq:gateway"
 IQVERSION     = "jabber:iq:version"
 IQREGISTER    = "jabber:iq:register"
 IQROSTER      = "jabber:iq:roster"
 IQAVATAR      = "jabber:iq:avatar"
+IQOOB         = "jabber:iq:oob"
+XOOB          = "jabber:x:oob"
 XCONFERENCE   = "jabber:x:conference"
 XEVENT        = "jabber:x:event"
 XDELAY        = "jabber:x:delay"
@@ -226,3 +232,46 @@ class ServerDiscovery:
                self.pytrans.send(el)
 
 
+class DiscoRequest:
+       def __init__(self, pytrans, jid):
+               LogEvent(INFO)
+               self.doDisco()
+       
+       def doDisco(self):
+               ID = self.pytrans.makeMessageID()
+               iq = Eleemnt((None, "iq"))
+               iq.attributes["to"] = jid
+               iq.attributes["from"] = config.jid
+               iq.attributes["type"] = "get"
+               query = iq.addElement("query")
+               query.attributes["xmlns"] = DISCO_INFO
+
+               d = self.pytrans.discovery.sendIq(iq)
+               d.addCallback(self.discoResponse)
+               d.addErrback(self.discoFail)
+               return d
+       
+       def discoResponse(self, el):
+               iqType = el.getAttribute("type")
+               if iqType != "result":
+                       return []
+
+               fro = el.getAttribute("from")
+
+               features = []
+
+               query = el.getElement("query")
+               if not query:
+                       return []
+
+               for child in query.elements():
+                       if child.name == "feature":
+                               features.append(child.getAttribute("var"))
+
+               return features
+       
+       def discoFail(self):
+               return []
+               
+
+
diff --git a/src/ft.py b/src/ft.py
new file mode 100644 (file)
index 0000000..c9faf03
--- /dev/null
+++ b/src/ft.py
@@ -0,0 +1,132 @@
+# Copyright 2005 James Bunton <james@delx.cjb.net>
+# Licensed for distribution under the GPL version 2, check COPYING for details
+
+import disco
+from debug import LogEvent, INFO, WARN, ERROR
+
+if utils.checkTwisted():
+       from twisted.xish.domish import Element
+else:
+       from tlib.domish import Element
+
+import random
+import sys
+
+class FTReceive:
+       """ Manager for file transfers going from MSN to Jabber. """
+
+       """
+       Plan of action for this class:
+       * Determine the FT support of the Jabber client.
+       * If we support a common protocol with them, create an
+         FTReceive_Invite object of that type. Either OOB (JEP0066) or SI(JEP0095)
+       * Call doInvite() and wait on the Deferred to send an affirmative or
+         negative to the MSN contact.
+       * The InvitationReceive sends IQ packets to the Jabber user to see if they
+         accept. If they do it creates an appropriate FTReceive_Transport to send
+         the file. Returning a Deferred for success or failure.
+
+       """
+
+       def __init__(self, session, senderJID, legacyftp):
+               self.session = session
+               self.senderJID = senderJID
+               self.legacyftp = legacyftp
+               LogEvent(INFO)
+               self.checkSupport()
+
+       def checkSupport(self):
+               def discoDone(features):
+                       c1 = features.count(disco.FT)
+                       c2 = features.count(disco.S5)
+                       c3 = features.count(disco.IBB)
+                       c4 = features.count(disco.IQOOB)
+                       #if c1 > 0 and c2 > 0:
+                       #       socksMode()
+                       #elif c1 > 0 and c3 > 0:
+                       #       ibbMode()
+                       if c4 > 0:
+                               oobMode()
+                       else:
+                               messageOobMode()
+
+               def discoFail(ignored=None):
+                       oobMode()
+               
+               d = disco.DiscoRequest(self.session.pytrans, self.session.jabberID)
+               d.addCallback(discoDone)
+               d.addErrback(discoFail)
+       
+       def socksMode(self):
+               LogEvent(ERROR)
+       
+       def ibbMode(self):
+               LogEvent(ERROR)
+       
+       def oobMode(self):
+               LogEvent(ERROR)
+
+       def messageOobMode(self):
+               global oobSite
+               self.legacyftp.accept()
+               filename = str(random.randint(0, sys.maxint))
+               oobSite.putFile(self, filename)
+               m = Element((None, "message"))
+               m.attributes["to"] = self.session.jabberID
+               m.attributes["from"] = self.senderJID
+               m.addElement("body").addContent(config.ftOOBRoot + "/" + filename)
+               x = m.addElement("x")
+               x.attributes["xmlns"] = disco.XOOB
+               x.addElement("url").addContent(config.ftOOBRoot + "/" + filename)
+               self.session.pytrans.send(m)
+       
+       def error(self):
+               #FIXME
+               
+
+
+
+# Put the files up for OOB download
+
+from twisted.web import server, resource, error
+from twisted.internet import reactor
+
+from debug import LogEvent, INFO, WARN, ERROR
+
+class Connector:
+       def __init__(self, ftReceive, ftHttpPush):
+               self.ftReceive, self.ftHttpPush = ftReceive, ftHttpPush
+               self.ftReceive.legacyftp.writeTo(self)
+       
+       def write(self, data):
+               self.ftHttpPush.write(data)
+       
+       def close(self):
+               self.ftHttpPush.finish()
+       
+       def error(self):
+               self.ftHttpPush.finish()
+               self.ftReceive.error()
+
+class FileTransfer(resource.Resource):
+       def __init__(self):
+               self.isLeaf = True
+               self.files = {}
+
+       def putFile(self, file, filename):
+               self.files[filename] = file
+       
+       def render_GET(self, request):
+               filename = "/" + request.path
+               if self.files.has_key(filename):
+                       file = self.files[filename]
+                       Connector(file, request)
+                       del self.files[filename]
+               else:
+                       page = error.NoResource(message="404 File Not Found")
+                       return page.render(request)
+
+oobSite = server.Site(FileTransfer())
+reactor.listenTCP(8080, site)
+
+
index a809f8eed0b6eb2e2f275d6deb34398ef8f39f6c..e155b8bc8e79d5ce89d34ada5cb70951d847d635 100644 (file)
@@ -11,6 +11,7 @@ from tlib import msn, msnp2p
 from debug import LogEvent, INFO, WARN, ERROR
 import sha
 import groupchat
+import ft
 import avatar
 import msnw
 import config
@@ -21,7 +22,7 @@ import lang
 
 name = "MSN Transport"   # The name of the transport
 url = "http://msn-transport.jabberstudio.org"
-version = "0.10.1"       # The transport version
+version = "0.11-dev"     # The transport version
 mangle = True            # XDB '@' -> '%' mangling
 id = "msn"               # The transport identifier
 
@@ -330,6 +331,11 @@ class LegacyConnection(msnw.MSNConnection):
                av = self.session.pytrans.avatarCache.setAvatar(imageData)
                c.updateAvatar(av)
        
+       def gotSendRequest(self, fileReceive):
+               if not self.session: return
+               LogEvent(INFO, self.session.jabberID)
+               ft.FTReceive(self.session, msn2jid(fileReceive).userHandle, fileReceive)
+       
        def loggedIn(self):
                if not self.session: return
                LogEvent(INFO, self.session.jabberID)
index 0644246d30ddc1f52d04829744f2f7333aa0a37e..a72f3ce777a6faeb3c3957de9a16b984534b3d6d 100644 (file)
@@ -186,6 +186,9 @@ class MSNConnection:
        def gotAvatarImage(self, to, image):
                pass
        
+       def gotSendRequest(self, fileReceive):
+               pass
+       
        def listSynchronized(self):
                pass
        
@@ -315,6 +318,9 @@ class SwitchboardSessionBase:
        
        def gotAvatarImage(self, to, image):
                self.msncon.gotAvatarImage(to, image)
+       
+       def gotSendRequest(self, fileReceive):
+               self.msncon.gotSendRequest(fileReceive)
 
        def switchboardReady(self, switchboard):
                LogEvent(INFO, self.ident)
@@ -815,5 +821,11 @@ class Switchboard(msn.SwitchboardClient):
        def gotAvatarImage(self, to, image):
                if self.badConditions(): return
                self.switchboardSession.gotAvatarImage(to, image)
+       
+       def gotSendRequest(self, fileReceive):
+               if self.badConditions():
+                       fileReceive.accept(False)
+                       return
+               self.switchboardSession.gotSendRequest(fileReceive)
 
  
index c5c9d399fc99df7db2c523a0d547c4833ce48ce2..56f615f7206f42f3b65502ee34ec3f89daa8ab17 100644 (file)
@@ -98,6 +98,7 @@ if(utils.checkTwisted()):
 else:
        from twisted.protocols.http import HTTPClient
 import msnp11chl
+import msnft
 import msnp2p
 
 # Twisted imports
@@ -979,7 +980,7 @@ class NotificationClient(MSNEventBase):
 
     def handle_CHL(self, params):
         checkParamLen(len(params), 2, 'CHL')
-       response = msnp11chl.doChallenge(params[1])
+        response = msnp11chl.doChallenge(params[1])
         self.sendLine("QRY %s %s %s" % (self._nextTransactionID(), msnp11chl.MSNP11_PRODUCT_ID, len(response)))
         self.transport.write(response)
 
@@ -1897,7 +1898,7 @@ class SwitchboardClient(MSNEventBase):
     def __init__(self, msnobj=None):
         MSNEventBase.__init__(self)
         self.pendingUsers = {}
-        self.cookies = {'iCookies' : {}, 'external' : {}} # will maybe be moved to a factory in the future
+        self.cookies = {'iCookies' : {}} # will maybe be moved to a factory in the future
         self.p2pHandlers = []
         self.msnobj = msnobj
 
@@ -1907,7 +1908,6 @@ class SwitchboardClient(MSNEventBase):
 
     def connectionLost(self, reason):
         self.cookies['iCookies'] = {}
-        self.cookies['external'] = {}
         MSNEventBase.connectionLost(self, reason)
 
     def _sendInit(self):
@@ -1942,36 +1942,7 @@ class SwitchboardClient(MSNEventBase):
         except KeyError:
             log.msg('Received munged file transfer request ... ignoring.')
             return 0
-        self.gotSendRequest(fileName, fileSize, cookie, message)
-        return 1
-
-    def _checkFileResponse(self, message, info):
-        """ helper method for checkMessage """
-        try:
-            cmd = info['Invitation-Command'].upper()
-            cookie = info['Invitation-Cookie']
-        except KeyError: return 0
-        accept = (cmd == 'ACCEPT') and 1 or 0
-        requested = self.cookies['iCookies'].get(cookie)
-        if not requested: return 1
-        requested[0].callback((accept, cookie, info))
-        del self.cookies['iCookies'][cookie]
-        return 1
-
-    def _checkFileInfo(self, message, info):
-        """ helper method for checkMessage """
-        try:
-            ip = info['IP-Address']
-            iCookie = info['Invitation-Cookie']
-            aCookie = info['AuthCookie']
-            cmd = info['Invitation-Command'].upper()
-            port = int(info['Port'])
-        except KeyError: return 0
-        accept = (cmd == 'ACCEPT') and 1 or 0
-        requested = self.cookies['external'].get(iCookie)
-        if not requested: return 1 # we didn't ask for this
-        requested[0].callback((accept, ip, port, aCookie, info))
-        del self.cookies['external'][iCookie]
+        self.gotSendRequest(msnft.MSNFTP_Receive(fileName, fileSize, message, message.userHandle, cookie, self))
         return 1
 
     def _checkP2PMessage(self, message, ctypes):
@@ -2017,7 +1988,7 @@ class SwitchboardClient(MSNEventBase):
                     key, val = line.split(':')
                     info[key] = val.lstrip()
                 except ValueError: continue
-            if self._checkFileInvitation(message, info) or self._checkFileInfo(message, info) or self._checkFileResponse(message, info): return 0
+            if self._checkFileInvitation(message, info): return 0
         if self._checkP2PMessage(message, cTypes): return 0
         return 1
 
@@ -2110,34 +2081,27 @@ class SwitchboardClient(MSNEventBase):
 
     def gotAvatarImage(self, userHandle, image):
         """
-        called when we receive an avatar from a user
+        called when we receive an avatar from a contact
 
         @param userHandle: the person who's avatar we have got
         @param image: the avatar image
         """
         pass
 
-    def userTyping(self, message):
+    def gotSendRequest(self, fileReceive):
         """
-        called when we receive the special type of message notifying
-        us that a user is typing a message.
+        called when we receive a file send request from a contact
 
-        @param message: the associated MSNMessage object
+        @param fileReceive: msnft.MSNFTReceive_Base instance
         """
         pass
 
-    def gotSendRequest(self, fileName, fileSize, iCookie, message):
+    def userTyping(self, message):
         """
-        called when a contact is trying to send us a file.
-        To accept or reject this transfer see the
-        fileInvitationReply method.
+        called when we receive the special type of message notifying
+        us that a user is typing a message.
 
-        @param fileName: the name of the file
-        @param fileSize: the size of the file
-        @param iCookie: the invitation cookie, used so the client can
-                        match up your reply with this request.
-        @param message: the MSNMessage object which brought about this
-                        invitation (it may contain more information)
+        @param message: the associated MSNMessage object
         """
         pass
 
@@ -2191,7 +2155,7 @@ class SwitchboardClient(MSNEventBase):
 
     def sendAvatarRequest(self, userHandle, msnobj):
         handler = msnp2p.MSNP2P_Avatar_Receive(to=userHandle, fro=self.userHandle, msnobj=msnobj)
-       self.p2pHandlers.append(handler)
+        self.p2pHandlers.append(handler)
         msnmessage = MSNMessage(message=handler.getNextPacket())
         msnmessage.setHeader("Content-Type", "application/x-msnmsgrp2p")
         msnmessage.setHeader("P2P-Dest", handler.to)
@@ -2248,39 +2212,6 @@ class SwitchboardClient(MSNEventBase):
         self.cookies['iCookies'][cookie] = (d, m)
         return d
 
-    def fileInvitationReply(self, iCookie, accept=1):
-        """
-        used to reply to a file transfer invitation.
-
-        @param iCookie: the invitation cookie of the initial invitation
-        @param accept: whether or not you accept this transfer,
-                       1 = yes, 0 = no, default = 1.
-
-        @return: A Deferred, the callback for which will be fired when
-                 the user responds with the transfer information.
-                 The callback argument will be a tuple with 5 elements,
-                 whether or not they wish to proceed with the transfer
-                 (1=yes, 0=no), their ip, the port, the authentication
-                 cookie (see FileReceive/FileSend) and the message
-                 info (dict) (in case they send extra header-like info
-                 like Internal-IP, this doesn't necessarily need to be
-                 used). If you wish to proceed with the transfer see
-                 FileReceive.
-        """
-        d = Deferred()
-        m = MSNMessage()
-        m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
-        m.message += 'Invitation-Command: %s\r\n' % (accept and 'ACCEPT' or 'CANCEL')
-        m.message += 'Invitation-Cookie: %s\r\n' % str(iCookie)
-        if not accept: m.message += 'Cancel-Code: REJECT\r\n'
-        m.message += 'Launch-Application: FALSE\r\n'
-        m.message += 'Request-Data: IP-Address:\r\n'
-        m.message += '\r\n'
-        m.ack = m.MESSAGE_ACK_NONE
-        self.sendMessage(m)
-        self.cookies['external'][iCookie] = (d, m)
-        return d
-
     def sendTransferInfo(self, accept, iCookie, authCookie, ip, port):
         """
         send information relating to a file transfer session.
@@ -2305,142 +2236,6 @@ class SwitchboardClient(MSNEventBase):
         m.ack = m.MESSAGE_NACK
         self.sendMessage(m)
 
-class FileReceive(LineReceiver):
-    """
-    This class provides support for receiving files from contacts.
-
-    @ivar fileSize: the size of the receiving file. (you will have to set this)
-    @ivar connected: true if a connection has been established.
-    @ivar completed: true if the transfer is complete.
-    @ivar bytesReceived: number of bytes (of the file) received.
-                         This does not include header data.
-    """
-
-    def __init__(self, auth, myUserHandle, file, directory="", overwrite=0):
-        """
-        @param auth: auth string received in the file invitation.
-        @param myUserHandle: your userhandle.
-        @param file: A string or file object represnting the file
-                     to save data to.
-        @param directory: optional parameter specifiying the directory.
-                          Defaults to the current directory.
-        @param overwrite: if true and a file of the same name exists on
-                          your system, it will be overwritten. (0 by default)
-        """
-        self.auth = auth
-        self.myUserHandle = myUserHandle
-        self.fileSize = 0
-        self.connected = 0
-        self.completed = 0
-        self.directory = directory
-        self.bytesReceived = 0
-        self.overwrite = overwrite
-
-        # used for handling current received state
-        self.state = 'CONNECTING'
-        self.segmentLength = 0
-        self.buffer = ''
-        
-        if isinstance(file, types.StringType):
-            path = os.path.join(directory, file)
-            if os.path.exists(path) and not self.overwrite:
-                log.msg('File already exists...')
-                raise IOError, "File Exists" # is this all we should do here?
-            self.file = open(os.path.join(directory, file), 'wb')
-        else:
-            self.file = file
-
-    def connectionMade(self):
-        self.connected = 1
-        self.state = 'INHEADER'
-        self.sendLine('VER MSNFTP')
-
-    def connectionLost(self, reason):
-        self.connected = 0
-        self.file.close()
-
-    def parseHeader(self, header):
-        """ parse the header of each 'message' to obtain the segment length """
-
-        if ord(header[0]) != 0: # they requested that we close the connection
-            self.transport.loseConnection()
-            return
-        try:
-            extra, factor = header[1:]
-        except ValueError:
-            # munged header, ending transfer
-            self.transport.loseConnection()
-            raise
-        extra  = ord(extra)
-        factor = ord(factor)
-        return factor * 256 + extra
-
-    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) # try/except
-        else: self.handle_UNKNOWN(cmd, params)
-
-    def rawDataReceived(self, data):
-        bufferLen = len(self.buffer)
-        if self.state == 'INHEADER':
-            delim = 3-bufferLen
-            self.buffer += data[:delim]
-            if len(self.buffer) == 3:
-                self.segmentLength = self.parseHeader(self.buffer)
-                if not self.segmentLength: return # hrm
-                self.buffer = ""
-                self.state = 'INSEGMENT'
-            extra = data[delim:]
-            if len(extra) > 0: self.rawDataReceived(extra)
-            return
-
-        elif self.state == 'INSEGMENT':
-            dataSeg = data[:(self.segmentLength-bufferLen)]
-            self.buffer += dataSeg
-            self.bytesReceived += len(dataSeg)
-            if len(self.buffer) == self.segmentLength:
-                self.gotSegment(self.buffer)
-                self.buffer = ""
-                if self.bytesReceived == self.fileSize:
-                    self.completed = 1
-                    self.buffer = ""
-                    self.file.close()
-                    self.sendLine("BYE 16777989")
-                    return
-                self.state = 'INHEADER'
-                extra = data[(self.segmentLength-bufferLen):]
-                if len(extra) > 0: self.rawDataReceived(extra)
-                return
-
-    def handle_VER(self, params):
-        checkParamLen(len(params), 1, 'VER')
-        if params[0].upper() == "MSNFTP":
-            self.sendLine("USR %s %s" % (self.myUserHandle, self.auth))
-        else:
-            log.msg('they sent the wrong version, time to quit this transfer')
-            self.transport.loseConnection()
-
-    def handle_FIL(self, params):
-        checkParamLen(len(params), 1, 'FIL')
-        try:
-            self.fileSize = int(params[0])
-        except ValueError: # they sent the wrong file size - probably want to log this
-            self.transport.loseConnection()
-            return
-        self.setRawMode()
-        self.sendLine("TFR")
-
-    def handle_UNKNOWN(self, cmd, params):
-        log.msg('received unknown command (%s), params: %s' % (cmd, params))
-
-    def gotSegment(self, data):
-        """ called when a segment (block) of data arrives. """
-        self.file.write(data)
-
 class FileSend(LineReceiver):
     """
     This class provides support for sending files to other contacts.
diff --git a/src/tlib/msnft.py b/src/tlib/msnft.py
new file mode 100644 (file)
index 0000000..da3af4d
--- /dev/null
@@ -0,0 +1,282 @@
+# Copyright 2005 James Bunton <james@delx.cjb.net>
+# Licensed for distribution under the GPL version 2, check COPYING for details
+
+from twisted.internet import reactor
+from twisted.protocols.basic import LineReceiver
+from twisted.internet.protocol import ClientFactory
+
+from msn import MSNMessage, MSNEventBase
+
+from debug import LogEvent, INFO, WARN, ERROR
+import config
+
+import random
+import sys
+
+
+class MSNFTReceive_Base:
+       # Public
+       def __init__(self, filename, filesize, message, userHandle):
+               self.connector = None
+               self.finished = False
+               self.error = False
+               self.buffer = []
+               self.filename, self.filesize, self.message, self.userHandle = filename, filesize, message, userHandle
+       
+       def removeMe(self):
+               self.connector = None
+       
+       def accept(self, yes):
+               pass
+       
+       def writeTo(self, obj):
+               self.connector = obj
+               for data in self.buffer:
+                       self.connector.write(data)
+               self.buffer = []
+               if self.finished:
+                       self.connector.close()
+               if self.error:
+                       self.connector.error()
+
+
+       # Private
+       def write(self, data):
+               if self.connector:
+                       self.connector.write(data)
+               else:
+                       self.buffer.append(data)
+       
+       def close(self):
+               self.removeMe()
+               self.finished = True
+               if self.connector:
+                       self.connector.close()
+       
+       def gotError(self, ignored=None):
+               self.removeMe()
+               self.error = True
+               if self.connector:
+                       self.connector.error()
+
+
+class MSNFTP_Ports:
+       def __init__(self):
+               try:
+                       lowPort = int(config.ftLowPort)
+                       highPort = int(config.ftHighPort)
+               except ValueError:
+                       LogEvent(ERROR, "", "Invalid values for ftLowPort & ftHighPort. Using 6891 & 6899 respectively")
+                       lowPort = 6891
+                       highPort = 6899
+               self.ports = [lowPort+x for x in xrange(highPort-lowPort)]
+               self.portFree = [False] * len(self.ports)
+       
+       def requestPort(self):
+               for i in xrange(len(self.ports)):
+                       if self.portFree[i]:
+                               self.portFree[i] = False
+                               LogEvent(INFO, "", "Reserved a port")
+                               return self.port[i]
+               LogEvent(INFO, "", "Out of ports")
+
+       def freePort(self, port):
+               if self.ports.count(port) > 0:
+                       self.portFree[self.ports.index(port)] = True;
+                       LogEvent(INFO)
+
+msnports = MSNFTP_Ports()
+
+class MSNFTP_Receive(ClientFactory):
+       def __init__(self. filename, filesize, userHandle, message, iCookie, switchboard):
+               MSNFTReceive_Base(filename, filesize, message, userHandle)
+               self.iCookie = iCookie
+               self.switchboard = switchboard
+               self.serverSocket = None
+               self.timeout = None
+               self.authCookie = str(random.randint(0, sys.maxint))
+               LogEvent(INFO, self.switchboard.userHandle)
+       
+       def removeMe(self):
+               if self.serverSocket:
+                       self.serverSocket.stopListening()
+               if self.timeout:
+                       self.timeout.cancel()
+               LogEvent(INFO, self.switchboard.userHandle)
+               
+       def accept(self, yes=True):
+               LogEvent(INFO, self.switchboard.userHandle)
+               global msnports
+               port = msnports.requestPort()
+               if not port:
+                       yes = False
+                       self.gotError()
+       
+               m = MSNMessage()
+               m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
+               if yes:
+                       m.message += 'IP-Address: %s\r\n' % str(config.ip)
+                       m.message += 'Port: %s\r\n' % str(port)
+                       m.message += 'AuthCookie %s\r\n' % authCookie
+                       m.message += 'Sender-Connect: TRUE\r\n'
+                       m.message += 'Invitation-Command: ACCEPT\r\n'
+                       m.message += 'Invitation-Cookie: %s\r\n' % str(self.iCookie)
+               else:
+                       m.message += 'Invitation-Command: CANCEL\r\n'
+                       m.message += 'Cancel-Code: REJECT\r\n'
+               m.message += 'Launch-Application: FALSE\r\n'
+               m.message += 'Request-Data: IP-Address:\r\n'
+               m.message += '\r\n'
+               m.ack = m.MESSAGE_ACK_NONE
+               self.switchboard.sendMessage(m)
+
+               if yes:
+                       self.serverSocket = reactor.listenTCP(port, self)
+                       self.timeout = reactor.callLater(20, self.gotError)
+       
+       def buildProtocol(self, addr):
+               LogEvent(INFO, self.switchboard.userHandle)
+               self.serverSocket.stopListening()
+               self.serverSocket = None
+               self.timeout.cancel()
+               self.timeout = None
+               return MSNFTP_FileReceive(authCookie, self.switchboard.userHandle, self)
+               
+
+class MSNFTP_FileReceive(LineReceiver):
+    """
+    This class provides support for receiving files from contacts.
+
+    @ivar fileSize: the size of the receiving file. (you will have to set this)
+    @ivar connected: true if a connection has been established.
+    @ivar completed: true if the transfer is complete.
+    @ivar bytesReceived: number of bytes (of the file) received.
+                         This does not include header data.
+    """
+
+    def __init__(self, auth, myUserHandle, file, directory="", overwrite=0):
+        """
+        @param auth: auth string received in the file invitation.
+        @param myUserHandle: your userhandle.
+        @param file: A string or file object represnting the file
+                     to save data to.
+        @param directory: optional parameter specifiying the directory.
+                          Defaults to the current directory.
+        @param overwrite: if true and a file of the same name exists on
+                          your system, it will be overwritten. (0 by default)
+        """
+        self.auth = auth
+        self.myUserHandle = myUserHandle
+        self.fileSize = 0
+        self.connected = 0
+        self.completed = 0
+        self.directory = directory
+        self.bytesReceived = 0
+        self.overwrite = overwrite
+
+        # used for handling current received state
+        self.state = 'CONNECTING'
+        self.segmentLength = 0
+        self.buffer = ''
+        
+        if isinstance(file, types.StringType):
+            path = os.path.join(directory, file)
+            if os.path.exists(path) and not self.overwrite:
+                log.msg('File already exists...')
+                raise IOError, "File Exists" # is this all we should do here?
+            self.file = open(os.path.join(directory, file), 'wb')
+        else:
+            self.file = file
+
+    def connectionMade(self):
+        self.connected = 1
+        self.state = 'INHEADER'
+        self.sendLine('VER MSNFTP')
+
+    def connectionLost(self, reason):
+        self.connected = 0
+        self.file.close()
+
+    def parseHeader(self, header):
+        """ parse the header of each 'message' to obtain the segment length """
+
+        if ord(header[0]) != 0: # they requested that we close the connection
+            self.transport.loseConnection()
+            return
+        try:
+            extra, factor = header[1:]
+        except ValueError:
+            # munged header, ending transfer
+            self.transport.loseConnection()
+            raise
+        extra  = ord(extra)
+        factor = ord(factor)
+        return factor * 256 + extra
+
+    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) # try/except
+        else: self.handle_UNKNOWN(cmd, params)
+
+    def rawDataReceived(self, data):
+        bufferLen = len(self.buffer)
+        if self.state == 'INHEADER':
+            delim = 3-bufferLen
+            self.buffer += data[:delim]
+            if len(self.buffer) == 3:
+                self.segmentLength = self.parseHeader(self.buffer)
+                if not self.segmentLength: return # hrm
+                self.buffer = ""
+                self.state = 'INSEGMENT'
+            extra = data[delim:]
+            if len(extra) > 0: self.rawDataReceived(extra)
+            return
+
+        elif self.state == 'INSEGMENT':
+            dataSeg = data[:(self.segmentLength-bufferLen)]
+            self.buffer += dataSeg
+            self.bytesReceived += len(dataSeg)
+            if len(self.buffer) == self.segmentLength:
+                self.gotSegment(self.buffer)
+                self.buffer = ""
+                if self.bytesReceived == self.fileSize:
+                    self.completed = 1
+                    self.buffer = ""
+                    self.file.close()
+                    self.sendLine("BYE 16777989")
+                    return
+                self.state = 'INHEADER'
+                extra = data[(self.segmentLength-bufferLen):]
+                if len(extra) > 0: self.rawDataReceived(extra)
+                return
+
+    def handle_VER(self, params):
+        checkParamLen(len(params), 1, 'VER')
+        if params[0].upper() == "MSNFTP":
+            self.sendLine("USR %s %s" % (self.myUserHandle, self.auth))
+        else:
+            log.msg('they sent the wrong version, time to quit this transfer')
+            self.transport.loseConnection()
+
+    def handle_FIL(self, params):
+        checkParamLen(len(params), 1, 'FIL')
+        try:
+            self.fileSize = int(params[0])
+        except ValueError: # they sent the wrong file size - probably want to log this
+            self.transport.loseConnection()
+            return
+        self.setRawMode()
+        self.sendLine("TFR")
+
+    def handle_UNKNOWN(self, cmd, params):
+        log.msg('received unknown command (%s), params: %s' % (cmd, params))
+
+    def gotSegment(self, data):
+        """ called when a segment (block) of data arrives. """
+        self.file.write(data)
+
+