diff options
Diffstat (limited to 'src/xmpppy-0.5.0rc1/xmpp/transports.py')
-rw-r--r-- | src/xmpppy-0.5.0rc1/xmpp/transports.py | 339 |
1 files changed, 339 insertions, 0 deletions
diff --git a/src/xmpppy-0.5.0rc1/xmpp/transports.py b/src/xmpppy-0.5.0rc1/xmpp/transports.py new file mode 100644 index 00000000..0e3eec90 --- /dev/null +++ b/src/xmpppy-0.5.0rc1/xmpp/transports.py @@ -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) |