Package xmpp :: Module transports

[hide private]
[frames] | no frames]

# Source Code for Module xmpp.transports

      1  ##   transports.py
      2  ##
      3  ##   Copyright (C) 2003-2004 Alexey "Snake" Nezhdanov
      4  ##
      5  ##   This program is free software; you can redistribute it and/or modify
      6  ##   it under the terms of the GNU General Public License as published by
      7  ##   the Free Software Foundation; either version 2, or (at your option)
      8  ##   any later version.
      9  ##
     10  ##   This program is distributed in the hope that it will be useful,
     11  ##   but WITHOUT ANY WARRANTY; without even the implied warranty of
     12  ##   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13  ##   GNU General Public License for more details.
     14
     15  # $Id: transports.py,v 1.31 2007/09/15 11:34:28 normanr Exp $
     16
     17  """
     18  This module contains the low-level implementations of xmpppy connect methods or
     19  (in other words) transports for xmpp-stanzas.
     20  Currently here is three transports:
     21  direct TCP connect - TCPsocket class
     22  proxied TCP connect - HTTPPROXYsocket class (CONNECT proxies)
     23  TLS connection - TLS class. Can be used for SSL connections also.
     24
     25  Transports are stackable so you - f.e. TLS use HTPPROXYsocket or TCPsocket as more low-level transport.
     26
     27  Also exception 'error' is defined to allow capture of this module specific exceptions.
     28  """
     29
     30  import socket,select,base64,dispatcher,sys
     31  from simplexml import ustr
     32  from client import PlugIn
     33  from protocol import *
     34
     35  # determine which DNS resolution library is available
     36  HAVE_DNSPYTHON = False
     37  HAVE_PYDNS = False
     38  try:
     39      import dns.resolver # http://dnspython.org/
     40      HAVE_DNSPYTHON = True
     41  except ImportError:
     42      try:
     43          import DNS # http://pydns.sf.net/
     44          HAVE_PYDNS = True
     45      except ImportError:
     46          #TODO: use self.DEBUG()
     47          sys.stderr.write("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")
     48
     49  DATA_RECEIVED='DATA RECEIVED'
     50  DATA_SENT='DATA SENT'
     51
     52 -class error:
     53      """An exception to be raised in case of low-level errors in methods of 'transports' module."""
     54 -    def __init__(self,comment):
     55          """Cache the descriptive string"""
     56          self._comment=comment
     57
     58 -    def __str__(self):
     59          """Serialise exception into pre-cached descriptive string."""
     60          return self._comment
     61
     62  BUFLEN=1024
     63 -class TCPsocket(PlugIn):
     64      """ This class defines direct TCP connection method. """
     65 -    def __init__(self, server=None, use_srv=True):
     66          """ Cache connection point 'server'. 'server' is the tuple of (host, port)
     67              absolutely the same as standard tcp socket uses. """
     68          PlugIn.__init__(self)
     69          self.DBG_LINE='socket'
     70          self._exported_methods=[self.send,self.disconnect]
     71
     72          # SRV resolver
     73          if use_srv and (HAVE_DNSPYTHON or HAVE_PYDNS):
     74              host, port = server
     75              possible_queries = ['_xmpp-client._tcp.' + host]
     76
     77              for query in possible_queries:
     78                  try:
     79                      if HAVE_DNSPYTHON:
     80                          answers = [x for x in dns.resolver.query(query, 'SRV')]
     81                          if answers:
     82                              host = str(answers[0].target)
     83                              port = int(answers[0].port)
     84                              break
     85                      elif HAVE_PYDNS:
     86                          # ensure we haven't cached an old configuration
     87                          DNS.ParseResolvConf()
     88                          response = DNS.Request().req(query, qtype='SRV')
     89                          answers = response.answers
     90                          if len(answers) > 0:
     91                              # ignore the priority and weight for now
     92                              _, _, port, host = answers[0]['data']
     93                              del _
     94                              port = int(port)
     95                              break
     96                  except:
     97                      #TODO: use self.DEBUG()
     98                      print 'An error occurred while looking up %s' % query
     99              server = (host, port)
    100          # end of SRV resolver
    101
    102          self._server = server
    103
    104 -    def plugin(self, owner):
    105          """ Fire up connection. Return non-empty string on success.
    106              Also registers self.disconnected method in the owner's dispatcher.
    107              Called internally. """
    108          if not self._server: self._server=(self._owner.Server,5222)
    109          if not self.connect(self._server): return
    110          self._owner.Connection=self
    111          self._owner.RegisterDisconnectHandler(self.disconnected)
    112          return 'ok'
    113
    114 -    def getHost(self):
    115          """ Return the 'host' value that is connection is [will be] made to."""
    116          return self._server[0]
    117 -    def getPort(self):
    118          """ Return the 'port' value that is connection is [will be] made to."""
    119          return self._server[1]
    120
    121 -    def connect(self,server=None):
    122          """ Try to connect. Returns non-empty string on success. """
    123          try:
    124              if not server: server=self._server
    125              self._sock=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    126              self._sock.connect((server[0], int(server[1])))
    127              self._send=self._sock.sendall
    128              self._recv=self._sock.recv
    129              self.DEBUG("Successfully connected to remote host %s"%`server`,'start')
    130              return 'ok'
    131          except socket.error, (errno, strerror):
    132              self.DEBUG("Failed to connect to remote host %s: %s (%s)"%(`server`, strerror, errno),'error')
    133          except: pass
    134
    135 -    def plugout(self):
    136          """ Disconnect from the remote server and unregister self.disconnected method from
    137              the owner's dispatcher. """
    138          self._sock.close()
    139          if self._owner.__dict__.has_key('Connection'):
    140              del self._owner.Connection
    141              self._owner.UnregisterDisconnectHandler(self.disconnected)
    142
    143 -    def receive(self):
    144          """ Reads all pending incoming data.
    145              In case of disconnection calls owner's disconnected() method and then raises IOError exception."""
    146          try: received = self._recv(BUFLEN)
    147          except socket.sslerror,e:
    148              self._seen_data=0
    149              if e[0]==socket.SSL_ERROR_WANT_READ: return ''
    150              if e[0]==socket.SSL_ERROR_WANT_WRITE: return ''
    151              self.DEBUG('Socket error while receiving data','error')
    152              sys.exc_clear()
    153              self._owner.disconnected()
    154              raise IOError("Disconnected from server")
    155          except: received = ''
    156
    157          while self.pending_data(0):
    158              try: add = self._recv(BUFLEN)
    159              except: add=''
    160              received +=add
    161              if not add: break
    162
    163          if len(received): # length of 0 means disconnect
    164              self._seen_data=1
    165              self.DEBUG(received,'got')
    166              if hasattr(self._owner, 'Dispatcher'):
    167                  self._owner.Dispatcher.Event('', DATA_RECEIVED, received)
    168          else:
    169              self.DEBUG('Socket error while receiving data','error')
    170              self._owner.disconnected()
    171              raise IOError("Disconnected from server")
    172          return received
    173
    174 -    def send(self,raw_data):
    175          """ Writes raw outgoing data. Blocks until done.
    176              If supplied data is unicode string, encodes it to utf-8 before send."""
    177          if type(raw_data)==type(u''): raw_data = raw_data.encode('utf-8')
    178          elif type(raw_data)<>type(''): raw_data = ustr(raw_data).encode('utf-8')
    179          try:
    180              self._send(raw_data)
    181              # Avoid printing messages that are empty keepalive packets.
    182              if raw_data.strip():
    183                  self.DEBUG(raw_data,'sent')
    184                  self._owner.Dispatcher.Event('', DATA_SENT, raw_data)
    185          except:
    186              self.DEBUG("Socket error while sending data",'error')
    187              self._owner.disconnected()
    188
    189 -    def pending_data(self,timeout=0):
    190          """ Returns true if there is a data ready to be read. """
    191          return select.select([self._sock],[],[],timeout)[0]
    192
    193 -    def disconnect(self):
    194          """ Closes the socket. """
    195          self.DEBUG("Closing socket",'stop')
    196          self._sock.close()
    197
    198 -    def disconnected(self):
    199          """ Called when a Network Error or disconnection occurs.
    200              Designed to be overidden. """
    201          self.DEBUG("Socket operation failed",'error')
    202
    203  DBG_CONNECT_PROXY='CONNECTproxy'
    204 -class HTTPPROXYsocket(TCPsocket):
    205      """ HTTP (CONNECT) proxy connection class. Uses TCPsocket as the base class
    206          redefines only connect method. Allows to use HTTP proxies like squid with
    207          (optionally) simple authentication (using login and password). """
    208 -    def __init__(self,proxy,server,use_srv=True):
    209          """ Caches proxy and target addresses.
    210              'proxy' argument is a dictionary with mandatory keys 'host' and 'port' (proxy address)
    211              and optional keys 'user' and 'password' to use for authentication.
    212              'server' argument is a tuple of host and port - just like TCPsocket uses. """
    213          TCPsocket.__init__(self,server,use_srv)
    214          self.DBG_LINE=DBG_CONNECT_PROXY
    215          self._proxy=proxy
    216
    217 -    def plugin(self, owner):
    218          """ Starts connection. Used interally. Returns non-empty string on success."""
    219          owner.debug_flags.append(DBG_CONNECT_PROXY)
    220          return TCPsocket.plugin(self,owner)
    221
    222 -    def connect(self,dupe=None):
    223          """ Starts connection. Connects to proxy, supplies login and password to it
    224              (if were specified while creating instance). Instructs proxy to make
    225              connection to the target server. Returns non-empty sting on success. """
    226          if not TCPsocket.connect(self,(self._proxy['host'],self._proxy['port'])): return
    227          self.DEBUG("Proxy server contacted, performing authentification",'start')
    228          connector = ['CONNECT %s:%s HTTP/1.0'%self._server,
    229              'Proxy-Connection: Keep-Alive',
    230              'Pragma: no-cache',
    231              'Host: %s:%s'%self._server,
    232              'User-Agent: HTTPPROXYsocket/v0.1']
    233          if self._proxy.has_key('user') and self._proxy.has_key('password'):
    234              credentials = '%s:%s'%(self._proxy['user'],self._proxy['password'])
    235              credentials = base64.encodestring(credentials).strip()
    236              connector.append('Proxy-Authorization: Basic '+credentials)
    237          connector.append('\r\n')
    238          self.send('\r\n'.join(connector))
    239          try: reply = self.receive().replace('\r','')
    240          except IOError:
    241              self.DEBUG('Proxy suddenly disconnected','error')
    242              self._owner.disconnected()
    243              return
    244          try: proto,code,desc=reply.split('\n')[0].split(' ',2)
    245          except: raise error('Invalid proxy reply')
    246          if code<>'200':
    247              self.DEBUG('Invalid proxy reply: %s %s %s'%(proto,code,desc),'error')
    248              self._owner.disconnected()
    249              return
    250          while reply.find('\n\n') == -1:
    251              try: reply += self.receive().replace('\r','')
    252              except IOError:
    253                  self.DEBUG('Proxy suddenly disconnected','error')
    254                  self._owner.disconnected()
    255                  return
    256          self.DEBUG("Authentification successfull. Jabber server contacted.",'ok')
    257          return 'ok'
    258
    259 -    def DEBUG(self,text,severity):
    260          """Overwrites DEBUG tag to allow debug output be presented as "CONNECTproxy"."""
    261          return self._owner.DEBUG(DBG_CONNECT_PROXY,text,severity)
    262
    263 -class TLS(PlugIn):
    264      """ TLS connection used to encrypts already estabilished tcp connection."""
    265 -    def PlugIn(self,owner,now=0):
    266          """ If the 'now' argument is true then starts using encryption immidiatedly.
    267              If 'now' in false then starts encryption as soon as TLS feature is
    268              declared by the server (if it were already declared - it is ok).
    269          """
    270          if owner.__dict__.has_key('TLS'): return  # Already enabled.
    271          PlugIn.PlugIn(self,owner)
    272          DBG_LINE='TLS'
    273          if now: return self._startSSL()
    274          if self._owner.Dispatcher.Stream.features:
    275              try: self.FeaturesHandler(self._owner.Dispatcher,self._owner.Dispatcher.Stream.features)
    276              except NodeProcessed: pass
    277          else: self._owner.RegisterHandlerOnce('features',self.FeaturesHandler,xmlns=NS_STREAMS)
    278          self.starttls=None
    279
    280 -    def plugout(self,now=0):
    281          """ Unregisters TLS handler's from owner's dispatcher. Take note that encription
    282              can not be stopped once started. You can only break the connection and start over."""
    283          self._owner.UnregisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS)
    284          self._owner.UnregisterHandler('proceed',self.StartTLSHandler,xmlns=NS_TLS)
    285          self._owner.UnregisterHandler('failure',self.StartTLSHandler,xmlns=NS_TLS)
    286
    287 -    def FeaturesHandler(self, conn, feats):
    288          """ Used to analyse server <features/> tag for TLS support.
    289              If TLS is supported starts the encryption negotiation. Used internally"""
    290          if not feats.getTag('starttls',namespace=NS_TLS):
    291              self.DEBUG("TLS unsupported by remote server.",'warn')
    292              return
    293          self.DEBUG("TLS supported by remote server. Requesting TLS start.",'ok')
    294          self._owner.RegisterHandlerOnce('proceed',self.StartTLSHandler,xmlns=NS_TLS)
    295          self._owner.RegisterHandlerOnce('failure',self.StartTLSHandler,xmlns=NS_TLS)
    296          self._owner.Connection.send('<starttls xmlns="%s"/>'%NS_TLS)
    297          raise NodeProcessed
    298
    299 -    def pending_data(self,timeout=0):
    300          """ Returns true if there possible is a data ready to be read. """
    301          return self._tcpsock._seen_data or select.select([self._tcpsock._sock],[],[],timeout)[0]
    302
    303 -    def _startSSL(self):
    304          """ Immidiatedly switch socket to TLS mode. Used internally."""
    305          """ Here we should switch pending_data to hint mode."""
    306          tcpsock=self._owner.Connection
    307          tcpsock._sslObj    = socket.ssl(tcpsock._sock, None, None)
    308          tcpsock._sslIssuer = tcpsock._sslObj.issuer()
    309          tcpsock._sslServer = tcpsock._sslObj.server()
    310          tcpsock._recv = tcpsock._sslObj.read
    311          tcpsock._send = tcpsock._sslObj.write
    312
    313          tcpsock._seen_data=1
    314          self._tcpsock=tcpsock
    315          tcpsock.pending_data=self.pending_data
    316          tcpsock._sock.setblocking(0)
    317
    318          self.starttls='success'
    319
    320 -    def StartTLSHandler(self, conn, starttls):
    321          """ Handle server reply if TLS is allowed to process. Behaves accordingly.
    322              Used internally."""
    323          if starttls.getNamespace()<>NS_TLS: return
    324          self.starttls=starttls.getName()
    325          if self.starttls=='failure':
    326              self.DEBUG("Got starttls response: "+self.starttls,'error')
    327              return
    328          self.DEBUG("Got starttls proceed response. Switching to TLS/SSL...",'ok')
    329          self._startSSL()
    330          self._owner.Dispatcher.PlugOut()
    331          dispatcher.Dispatcher().PlugIn(self._owner)
    332