xmpp: import xmppy-0.5.0rc1
[uccdoor.git] / xmpp / transports.py
diff --git a/xmpp/transports.py b/xmpp/transports.py
new file mode 100644 (file)
index 0000000..0e3eec9
--- /dev/null
@@ -0,0 +1,339 @@
+##   transports.py
+##
+##   Copyright (C) 2003-2004 Alexey "Snake" Nezhdanov
+##
+##   This program is free software; you can redistribute it and/or modify
+##   it under the terms of the GNU General Public License as published by
+##   the Free Software Foundation; either version 2, or (at your option)
+##   any later version.
+##
+##   This program is distributed in the hope that it will be useful,
+##   but WITHOUT ANY WARRANTY; without even the implied warranty of
+##   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+##   GNU General Public License for more details.
+
+# $Id: transports.py,v 1.35 2009/04/07 08:34:09 snakeru Exp $
+
+"""
+This module contains the low-level implementations of xmpppy connect methods or
+(in other words) transports for xmpp-stanzas.
+Currently here is three transports:
+direct TCP connect - TCPsocket class
+proxied TCP connect - HTTPPROXYsocket class (CONNECT proxies)
+TLS connection - TLS class. Can be used for SSL connections also.
+
+Transports are stackable so you - f.e. TLS use HTPPROXYsocket or TCPsocket as more low-level transport.
+
+Also exception 'error' is defined to allow capture of this module specific exceptions.
+"""
+
+import socket,select,base64,dispatcher,sys
+from simplexml import ustr
+from client import PlugIn
+from protocol import *
+
+# determine which DNS resolution library is available
+HAVE_DNSPYTHON = False
+HAVE_PYDNS = False
+try:
+    import dns.resolver # http://dnspython.org/
+    HAVE_DNSPYTHON = True
+except ImportError:
+    try:
+        import DNS # http://pydns.sf.net/
+        HAVE_PYDNS = True
+    except ImportError:
+        pass
+
+DATA_RECEIVED='DATA RECEIVED'
+DATA_SENT='DATA SENT'
+
+class error:
+    """An exception to be raised in case of low-level errors in methods of 'transports' module."""
+    def __init__(self,comment):
+        """Cache the descriptive string"""
+        self._comment=comment
+
+    def __str__(self):
+        """Serialise exception into pre-cached descriptive string."""
+        return self._comment
+
+BUFLEN=1024
+class TCPsocket(PlugIn):
+    """ This class defines direct TCP connection method. """
+    def __init__(self, server=None, use_srv=True):
+        """ Cache connection point 'server'. 'server' is the tuple of (host, port)
+            absolutely the same as standard tcp socket uses. However library will lookup for 
+            ('_xmpp-client._tcp.' + host) SRV record in DNS and connect to the found (if it is)
+            server instead
+        """
+        PlugIn.__init__(self)
+        self.DBG_LINE='socket'
+        self._exported_methods=[self.send,self.disconnect]
+        self._server, self.use_srv = server, use_srv
+
+    def srv_lookup(self, server):
+        " SRV resolver. Takes server=(host, port) as argument. Returns new (host, port) pair "
+        if HAVE_DNSPYTHON or HAVE_PYDNS:
+            host, port = server
+            possible_queries = ['_xmpp-client._tcp.' + host]
+
+            for query in possible_queries:
+                try:
+                    if HAVE_DNSPYTHON:
+                        answers = [x for x in dns.resolver.query(query, 'SRV')]
+                        if answers:
+                            host = str(answers[0].target)
+                            port = int(answers[0].port)
+                            break
+                    elif HAVE_PYDNS:
+                        # ensure we haven't cached an old configuration
+                        DNS.DiscoverNameServers()
+                        response = DNS.Request().req(query, qtype='SRV')
+                        answers = response.answers
+                        if len(answers) > 0:
+                            # ignore the priority and weight for now
+                            _, _, port, host = answers[0]['data']
+                            del _
+                            port = int(port)
+                            break
+                except:
+                    self.DEBUG('An error occurred while looking up %s' % query, 'warn')
+            server = (host, port)
+        else:
+            self.DEBUG("Could not load one of the supported DNS libraries (dnspython or pydns). SRV records will not be queried and you may need to set custom hostname/port for some servers to be accessible.\n",'warn')
+        # end of SRV resolver
+        return server
+
+    def plugin(self, owner):
+        """ Fire up connection. Return non-empty string on success.
+            Also registers self.disconnected method in the owner's dispatcher.
+            Called internally. """
+        if not self._server: self._server=(self._owner.Server,5222)
+        if self.use_srv: server=self.srv_lookup(self._server)
+        else: server=self._server
+        if not self.connect(server): return
+        self._owner.Connection=self
+        self._owner.RegisterDisconnectHandler(self.disconnected)
+        return 'ok'
+
+    def getHost(self):
+        """ Return the 'host' value that is connection is [will be] made to."""
+        return self._server[0]
+    def getPort(self):
+        """ Return the 'port' value that is connection is [will be] made to."""
+        return self._server[1]
+
+    def connect(self,server=None):
+        """ Try to connect to the given host/port. Does not lookup for SRV record.
+            Returns non-empty string on success. """
+        try:
+            if not server: server=self._server
+            self._sock=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+            self._sock.connect((server[0], int(server[1])))
+            self._send=self._sock.sendall
+            self._recv=self._sock.recv
+            self.DEBUG("Successfully connected to remote host %s"%`server`,'start')
+            return 'ok'
+        except socket.error, (errno, strerror): 
+            self.DEBUG("Failed to connect to remote host %s: %s (%s)"%(`server`, strerror, errno),'error')
+        except: pass
+
+    def plugout(self):
+        """ Disconnect from the remote server and unregister self.disconnected method from
+            the owner's dispatcher. """
+        self._sock.close()
+        if self._owner.__dict__.has_key('Connection'):
+            del self._owner.Connection
+            self._owner.UnregisterDisconnectHandler(self.disconnected)
+
+    def receive(self):
+        """ Reads all pending incoming data.
+            In case of disconnection calls owner's disconnected() method and then raises IOError exception."""
+        try: received = self._recv(BUFLEN)
+        except socket.sslerror,e:
+            self._seen_data=0
+            if e[0]==socket.SSL_ERROR_WANT_READ: return ''
+            if e[0]==socket.SSL_ERROR_WANT_WRITE: return ''
+            self.DEBUG('Socket error while receiving data','error')
+            sys.exc_clear()
+            self._owner.disconnected()
+            raise IOError("Disconnected from server")
+        except: received = ''
+
+        while self.pending_data(0):
+            try: add = self._recv(BUFLEN)
+            except: add=''
+            received +=add
+            if not add: break
+
+        if len(received): # length of 0 means disconnect
+            self._seen_data=1
+            self.DEBUG(received,'got')
+            if hasattr(self._owner, 'Dispatcher'):
+                self._owner.Dispatcher.Event('', DATA_RECEIVED, received)
+        else:
+            self.DEBUG('Socket error while receiving data','error')
+            self._owner.disconnected()
+            raise IOError("Disconnected from server")
+        return received
+
+    def send(self,raw_data):
+        """ Writes raw outgoing data. Blocks until done.
+            If supplied data is unicode string, encodes it to utf-8 before send."""
+        if type(raw_data)==type(u''): raw_data = raw_data.encode('utf-8')
+        elif type(raw_data)<>type(''): raw_data = ustr(raw_data).encode('utf-8')
+        try:
+            self._send(raw_data)
+            # Avoid printing messages that are empty keepalive packets.
+            if raw_data.strip():
+                self.DEBUG(raw_data,'sent')
+                if hasattr(self._owner, 'Dispatcher'): # HTTPPROXYsocket will send data before we have a Dispatcher
+                    self._owner.Dispatcher.Event('', DATA_SENT, raw_data)
+        except:
+            self.DEBUG("Socket error while sending data",'error')
+            self._owner.disconnected()
+
+    def pending_data(self,timeout=0):
+        """ Returns true if there is a data ready to be read. """
+        return select.select([self._sock],[],[],timeout)[0]
+
+    def disconnect(self):
+        """ Closes the socket. """
+        self.DEBUG("Closing socket",'stop')
+        self._sock.close()
+
+    def disconnected(self):
+        """ Called when a Network Error or disconnection occurs.
+            Designed to be overidden. """
+        self.DEBUG("Socket operation failed",'error')
+
+DBG_CONNECT_PROXY='CONNECTproxy'
+class HTTPPROXYsocket(TCPsocket):
+    """ HTTP (CONNECT) proxy connection class. Uses TCPsocket as the base class
+        redefines only connect method. Allows to use HTTP proxies like squid with
+        (optionally) simple authentication (using login and password). """
+    def __init__(self,proxy,server,use_srv=True):
+        """ Caches proxy and target addresses.
+            'proxy' argument is a dictionary with mandatory keys 'host' and 'port' (proxy address)
+            and optional keys 'user' and 'password' to use for authentication.
+            'server' argument is a tuple of host and port - just like TCPsocket uses. """
+        TCPsocket.__init__(self,server,use_srv)
+        self.DBG_LINE=DBG_CONNECT_PROXY
+        self._proxy=proxy
+
+    def plugin(self, owner):
+        """ Starts connection. Used interally. Returns non-empty string on success."""
+        owner.debug_flags.append(DBG_CONNECT_PROXY)
+        return TCPsocket.plugin(self,owner)
+
+    def connect(self,dupe=None):
+        """ Starts connection. Connects to proxy, supplies login and password to it
+            (if were specified while creating instance). Instructs proxy to make
+            connection to the target server. Returns non-empty sting on success. """
+        if not TCPsocket.connect(self,(self._proxy['host'],self._proxy['port'])): return
+        self.DEBUG("Proxy server contacted, performing authentification",'start')
+        connector = ['CONNECT %s:%s HTTP/1.0'%self._server,
+            'Proxy-Connection: Keep-Alive',
+            'Pragma: no-cache',
+            'Host: %s:%s'%self._server,
+            'User-Agent: HTTPPROXYsocket/v0.1']
+        if self._proxy.has_key('user') and self._proxy.has_key('password'):
+            credentials = '%s:%s'%(self._proxy['user'],self._proxy['password'])
+            credentials = base64.encodestring(credentials).strip()
+            connector.append('Proxy-Authorization: Basic '+credentials)
+        connector.append('\r\n')
+        self.send('\r\n'.join(connector))
+        try: reply = self.receive().replace('\r','')
+        except IOError:
+            self.DEBUG('Proxy suddenly disconnected','error')
+            self._owner.disconnected()
+            return
+        try: proto,code,desc=reply.split('\n')[0].split(' ',2)
+        except: raise error('Invalid proxy reply')
+        if code<>'200':
+            self.DEBUG('Invalid proxy reply: %s %s %s'%(proto,code,desc),'error')
+            self._owner.disconnected()
+            return
+        while reply.find('\n\n') == -1:
+            try: reply += self.receive().replace('\r','')
+            except IOError:
+                self.DEBUG('Proxy suddenly disconnected','error')
+                self._owner.disconnected()
+                return
+        self.DEBUG("Authentification successfull. Jabber server contacted.",'ok')
+        return 'ok'
+
+    def DEBUG(self,text,severity):
+        """Overwrites DEBUG tag to allow debug output be presented as "CONNECTproxy"."""
+        return self._owner.DEBUG(DBG_CONNECT_PROXY,text,severity)
+
+class TLS(PlugIn):
+    """ TLS connection used to encrypts already estabilished tcp connection."""
+    def PlugIn(self,owner,now=0):
+        """ If the 'now' argument is true then starts using encryption immidiatedly.
+            If 'now' in false then starts encryption as soon as TLS feature is
+            declared by the server (if it were already declared - it is ok).
+        """
+        if owner.__dict__.has_key('TLS'): return  # Already enabled.
+        PlugIn.PlugIn(self,owner)
+        DBG_LINE='TLS'
+        if now: return self._startSSL()
+        if self._owner.Dispatcher.Stream.features:
+            try: self.FeaturesHandler(self._owner.Dispatcher,self._owner.Dispatcher.Stream.features)
+            except NodeProcessed: pass
+        else: self._owner.RegisterHandlerOnce('features',self.FeaturesHandler,xmlns=NS_STREAMS)
+        self.starttls=None
+
+    def plugout(self,now=0):
+        """ Unregisters TLS handler's from owner's dispatcher. Take note that encription
+            can not be stopped once started. You can only break the connection and start over."""
+        self._owner.UnregisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS)
+        self._owner.UnregisterHandler('proceed',self.StartTLSHandler,xmlns=NS_TLS)
+        self._owner.UnregisterHandler('failure',self.StartTLSHandler,xmlns=NS_TLS)
+
+    def FeaturesHandler(self, conn, feats):
+        """ Used to analyse server <features/> tag for TLS support.
+            If TLS is supported starts the encryption negotiation. Used internally"""
+        if not feats.getTag('starttls',namespace=NS_TLS):
+            self.DEBUG("TLS unsupported by remote server.",'warn')
+            return
+        self.DEBUG("TLS supported by remote server. Requesting TLS start.",'ok')
+        self._owner.RegisterHandlerOnce('proceed',self.StartTLSHandler,xmlns=NS_TLS)
+        self._owner.RegisterHandlerOnce('failure',self.StartTLSHandler,xmlns=NS_TLS)
+        self._owner.Connection.send('<starttls xmlns="%s"/>'%NS_TLS)
+        raise NodeProcessed
+
+    def pending_data(self,timeout=0):
+        """ Returns true if there possible is a data ready to be read. """
+        return self._tcpsock._seen_data or select.select([self._tcpsock._sock],[],[],timeout)[0]
+
+    def _startSSL(self):
+        """ Immidiatedly switch socket to TLS mode. Used internally."""
+        """ Here we should switch pending_data to hint mode."""
+        tcpsock=self._owner.Connection
+        tcpsock._sslObj    = socket.ssl(tcpsock._sock, None, None)
+        tcpsock._sslIssuer = tcpsock._sslObj.issuer()
+        tcpsock._sslServer = tcpsock._sslObj.server()
+        tcpsock._recv = tcpsock._sslObj.read
+        tcpsock._send = tcpsock._sslObj.write
+
+        tcpsock._seen_data=1
+        self._tcpsock=tcpsock
+        tcpsock.pending_data=self.pending_data
+        tcpsock._sock.setblocking(0)
+
+        self.starttls='success'
+
+    def StartTLSHandler(self, conn, starttls):
+        """ Handle server reply if TLS is allowed to process. Behaves accordingly.
+            Used internally."""
+        if starttls.getNamespace()<>NS_TLS: return
+        self.starttls=starttls.getName()
+        if self.starttls=='failure':
+            self.DEBUG("Got starttls response: "+self.starttls,'error')
+            return
+        self.DEBUG("Got starttls proceed response. Switching to TLS/SSL...",'ok')
+        self._startSSL()
+        self._owner.Dispatcher.PlugOut()
+        dispatcher.Dispatcher().PlugIn(self._owner)

UCC git Repository :: git.ucc.asn.au