Package xmpp :: Module client

[hide private]
[frames] | no frames]

# Source Code for Module xmpp.client

      1  ##   client.py
      2  ##
      3  ##   Copyright (C) 2003-2005 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: client.py,v 1.60 2007/08/28 10:03:33 normanr Exp $
     16
     17  """
     18  Provides PlugIn class functionality to develop extentions for xmpppy.
     19  Also provides Client and Component classes implementations as the
     20  examples of xmpppy structures usage.
     21  These classes can be used for simple applications "AS IS" though.
     22  """
     23
     24  import socket
     25  import debug
     26  Debug=debug
     27  Debug.DEBUGGING_IS_ON=1
     28  Debug.Debug.colors['socket']=debug.color_dark_gray
     29  Debug.Debug.colors['CONNECTproxy']=debug.color_dark_gray
     30  Debug.Debug.colors['nodebuilder']=debug.color_brown
     31  Debug.Debug.colors['client']=debug.color_cyan
     32  Debug.Debug.colors['component']=debug.color_cyan
     33  Debug.Debug.colors['dispatcher']=debug.color_green
     34  Debug.Debug.colors['browser']=debug.color_blue
     35  Debug.Debug.colors['auth']=debug.color_yellow
     36  Debug.Debug.colors['roster']=debug.color_magenta
     37  Debug.Debug.colors['ibb']=debug.color_yellow
     38
     39  Debug.Debug.colors['down']=debug.color_brown
     40  Debug.Debug.colors['up']=debug.color_brown
     41  Debug.Debug.colors['data']=debug.color_brown
     42  Debug.Debug.colors['ok']=debug.color_green
     43  Debug.Debug.colors['warn']=debug.color_yellow
     44  Debug.Debug.colors['error']=debug.color_red
     45  Debug.Debug.colors['start']=debug.color_dark_gray
     46  Debug.Debug.colors['stop']=debug.color_dark_gray
     47  Debug.Debug.colors['sent']=debug.color_yellow
     48  Debug.Debug.colors['got']=debug.color_bright_cyan
     49
     50  DBG_CLIENT='client'
     51  DBG_COMPONENT='component'
     52
     53 -class PlugIn:
     54      """ Common xmpppy plugins infrastructure: plugging in/out, debugging. """
     55 -    def __init__(self):
     56          self._exported_methods=[]
     57          self.DBG_LINE=self.__class__.__name__.lower()
     58
     59 -    def PlugIn(self,owner):
     60          """ Attach to main instance and register ourself and all our staff in it. """
     61          self._owner=owner
     62          if self.DBG_LINE not in owner.debug_flags:
     63              owner.debug_flags.append(self.DBG_LINE)
     64          self.DEBUG('Plugging %s into %s'%(self,self._owner),'start')
     65          if owner.__dict__.has_key(self.__class__.__name__):
     66              return self.DEBUG('Plugging ignored: another instance already plugged.','error')
     67          self._old_owners_methods=[]
     68          for method in self._exported_methods:
     69              if owner.__dict__.has_key(method.__name__):
     70                  self._old_owners_methods.append(owner.__dict__[method.__name__])
     71              owner.__dict__[method.__name__]=method
     72          owner.__dict__[self.__class__.__name__]=self
     73          if self.__class__.__dict__.has_key('plugin'): return self.plugin(owner)
     74
     75 -    def PlugOut(self):
     76          """ Unregister all our staff from main instance and detach from it. """
     77          self.DEBUG('Plugging %s out of %s.'%(self,self._owner),'stop')
     78          ret = None
     79          if self.__class__.__dict__.has_key('plugout'): ret = self.plugout()
     80          self._owner.debug_flags.remove(self.DBG_LINE)
     81          for method in self._exported_methods: del self._owner.__dict__[method.__name__]
     82          for method in self._old_owners_methods: self._owner.__dict__[method.__name__]=method
     83          del self._owner.__dict__[self.__class__.__name__]
     84          return ret
     85
     86 -    def DEBUG(self,text,severity='info'):
     87          """ Feed a provided debug line to main instance's debug facility along with our ID string. """
     88          self._owner.DEBUG(self.DBG_LINE,text,severity)
     89
     90  import transports,dispatcher,auth,roster
     91 -class CommonClient:
     92      """ Base for Client and Component classes."""
     93 -    def __init__(self,server,port=5222,debug=['always', 'nodebuilder']):
     94          """ Caches server name and (optionally) port to connect to. "debug" parameter specifies
     95              the debug IDs that will go into debug output. You can either specifiy an "include"
     96              or "exclude" list. The latter is done via adding "always" pseudo-ID to the list.
     97              Full list: ['nodebuilder', 'dispatcher', 'gen_auth', 'SASL_auth', 'bind', 'socket',
     98               'CONNECTproxy', 'TLS', 'roster', 'browser', 'ibb'] . """
     99          if self.__class__.__name__=='Client': self.Namespace,self.DBG='jabber:client',DBG_CLIENT
    100          elif self.__class__.__name__=='Component': self.Namespace,self.DBG=dispatcher.NS_COMPONENT_ACCEPT,DBG_COMPONENT
    101          self.defaultNamespace=self.Namespace
    102          self.disconnect_handlers=[]
    103          self.Server=server
    104          self.Port=port
    105          if debug and type(debug)<>list: debug=['always', 'nodebuilder']
    106          self._DEBUG=Debug.Debug(debug)
    107          self.DEBUG=self._DEBUG.Show
    108          self.debug_flags=self._DEBUG.debug_flags
    109          self.debug_flags.append(self.DBG)
    110          self._owner=self
    111          self._registered_name=None
    112          self.RegisterDisconnectHandler(self.DisconnectHandler)
    113          self.connected=''
    114          self._route=0
    115
    116 -    def RegisterDisconnectHandler(self,handler):
    117          """ Register handler that will be called on disconnect."""
    118          self.disconnect_handlers.append(handler)
    119
    120 -    def UnregisterDisconnectHandler(self,handler):
    121          """ Unregister handler that is called on disconnect."""
    122          self.disconnect_handlers.remove(handler)
    123
    124 -    def disconnected(self):
    125          """ Called on disconnection. Calls disconnect handlers and cleans things up. """
    126          self.connected=''
    127          self.DEBUG(self.DBG,'Disconnect detected','stop')
    128          self.disconnect_handlers.reverse()
    129          for i in self.disconnect_handlers: i()
    130          self.disconnect_handlers.reverse()
    131          if self.__dict__.has_key('TLS'): self.TLS.PlugOut()
    132
    133 -    def DisconnectHandler(self):
    134          """ Default disconnect handler. Just raises an IOError.
    135              If you choosed to use this class in your production client,
    136              override this method or at least unregister it. """
    137          raise IOError('Disconnected from server.')
    138
    139 -    def event(self,eventName,args={}):
    140          """ Default event handler. To be overriden. """
    141          print "Event: ",(eventName,args)
    142
    143 -    def isConnected(self):
    144          """ Returns connection state. F.e.: None / 'tls' / 'tcp+non_sasl' . """
    145          return self.connected
    146
    147 -    def reconnectAndReauth(self):
    148          """ Example of reconnection method. In fact, it can be used to batch connection and auth as well. """
    149          handlerssave=self.Dispatcher.dumpHandlers()
    150          if self.__dict__.has_key('ComponentBind'): self.ComponentBind.PlugOut()
    151          if self.__dict__.has_key('Bind'): self.Bind.PlugOut()
    152          self._route=0
    153          if self.__dict__.has_key('NonSASL'): self.NonSASL.PlugOut()
    154          if self.__dict__.has_key('SASL'): self.SASL.PlugOut()
    155          if self.__dict__.has_key('TLS'): self.TLS.PlugOut()
    156          self.Dispatcher.PlugOut()
    157          if self.__dict__.has_key('HTTPPROXYsocket'): self.HTTPPROXYsocket.PlugOut()
    158          if self.__dict__.has_key('TCPsocket'): self.TCPsocket.PlugOut()
    159          if not self.connect(server=self._Server,proxy=self._Proxy): return
    160          if not self.auth(self._User,self._Password,self._Resource): return
    161          self.Dispatcher.restoreHandlers(handlerssave)
    162          return self.connected
    163
    164 -    def connect(self,server=None,proxy=None,ssl=None,use_srv=None):
    165          """ Make a tcp/ip connection, protect it with tls/ssl if possible and start XMPP stream.
    166              Returns None or 'tcp' or 'tls', depending on the result."""
    167          if not server: server=(self.Server,self.Port)
    168          if proxy: sock=transports.HTTPPROXYsocket(proxy,server,use_srv)
    169          else: sock=transports.TCPsocket(server,use_srv)
    170          connected=sock.PlugIn(self)
    171          if not connected:
    172              sock.PlugOut()
    173              return
    174          self._Server,self._Proxy=server,proxy
    175          self.connected='tcp'
    176          if (ssl is None and self.Connection.getPort() in (5223, 443)) or ssl:
    177              try:               # FIXME. This should be done in transports.py
    178                  transports.TLS().PlugIn(self,now=1)
    179                  self.connected='ssl'
    180              except socket.sslerror:
    181                  return
    182          dispatcher.Dispatcher().PlugIn(self)
    183          while self.Dispatcher.Stream._document_attrs is None:
    184              if not self.Process(1): return
    185          if self.Dispatcher.Stream._document_attrs.has_key('version') and self.Dispatcher.Stream._document_attrs['version']=='1.0':
    186              while not self.Dispatcher.Stream.features and self.Process(1): pass      # If we get version 1.0 stream the features tag MUST BE presented
    187          return self.connected
    188
    189 -class Client(CommonClient):
    190      """ Example client class, based on CommonClient. """
    191 -    def connect(self,server=None,proxy=None,secure=None,use_srv=True):
    192          """ Connect to jabber server. If you want to specify different ip/port to connect to you can
    193              pass it as tuple as first parameter. If there is HTTP proxy between you and server
    194              specify it's address and credentials (if needed) in the second argument.
    195              If you want ssl/tls support to be discovered and enable automatically - leave third argument as None. (ssl will be autodetected only if port is 5223 or 443)
    196              If you want to force SSL start (i.e. if port 5223 or 443 is remapped to some non-standard port) then set it to 1.
    197              If you want to disable tls/ssl support completely, set it to 0.
    198              Example: connect(('192.168.5.5',5222),{'host':'proxy.my.net','port':8080,'user':'me','password':'secret'})
    199              Returns '' or 'tcp' or 'tls', depending on the result."""
    200          if not CommonClient.connect(self,server,proxy,secure,use_srv) or secure<>None and not secure: return self.connected
    201          transports.TLS().PlugIn(self)
    202          if not self.Dispatcher.Stream._document_attrs.has_key('version') or not self.Dispatcher.Stream._document_attrs['version']=='1.0': return self.connected
    203          while not self.Dispatcher.Stream.features and self.Process(1): pass      # If we get version 1.0 stream the features tag MUST BE presented
    204          if not self.Dispatcher.Stream.features.getTag('starttls'): return self.connected       # TLS not supported by server
    205          while not self.TLS.starttls and self.Process(1): pass
    206          if not hasattr(self, 'TLS') or self.TLS.starttls!='success': self.event('tls_failed'); return self.connected
    207          self.connected='tls'
    208          return self.connected
    209
    210 -    def auth(self,user,password,resource='',sasl=1):
    211          """ Authenticate connnection and bind resource. If resource is not provided
    212              random one or library name used. """
    213          self._User,self._Password,self._Resource=user,password,resource
    214          while not self.Dispatcher.Stream._document_attrs and self.Process(1): pass
    215          if self.Dispatcher.Stream._document_attrs.has_key('version') and self.Dispatcher.Stream._document_attrs['version']=='1.0':
    216              while not self.Dispatcher.Stream.features and self.Process(1): pass      # If we get version 1.0 stream the features tag MUST BE presented
    217          if sasl: auth.SASL(user,password).PlugIn(self)
    218          if not sasl or self.SASL.startsasl=='not-supported':
    219              if not resource: resource='xmpppy'
    220              if auth.NonSASL(user,password,resource).PlugIn(self):
    221                  self.connected+='+old_auth'
    222                  return 'old_auth'
    223              return
    224          self.SASL.auth()
    225          while self.SASL.startsasl=='in-process' and self.Process(1): pass
    226          if self.SASL.startsasl=='success':
    227              auth.Bind().PlugIn(self)
    228              while self.Bind.bound is None and self.Process(1): pass
    229              if self.Bind.Bind(resource):
    230                  self.connected+='+sasl'
    231                  return 'sasl'
    232          else:
    233              if self.__dict__.has_key('SASL'): self.SASL.PlugOut()
    234
    235 -    def getRoster(self):
    236          """ Return the Roster instance, previously plugging it in and
    237              requesting roster from server if needed. """
    238          if not self.__dict__.has_key('Roster'): roster.Roster().PlugIn(self)
    239          return self.Roster.getRoster()
    240
    241 -    def sendInitPresence(self,requestRoster=1):
    242          """ Send roster request and initial <presence/>.
    243              You can disable the first by setting requestRoster argument to 0. """
    244          self.sendPresence(requestRoster=requestRoster)
    245
    246 -    def sendPresence(self,jid=None,typ=None,requestRoster=0):
    247          """ Send some specific presence state.
    248              Can also request roster from server if according agrument is set."""
    249          if requestRoster: roster.Roster().PlugIn(self)
    250          self.send(dispatcher.Presence(to=jid, typ=typ))
    251
    252 -class Component(CommonClient):
    253      """ Component class. The only difference from CommonClient is ability to perform component authentication. """
    254 -    def __init__(self,server,port=5347,typ=None,debug=['always', 'nodebuilder'],domains=None,sasl=0,bind=0,route=0,xcp=0):
    255          """ Init function for Components.
    256              As components use a different auth mechanism which includes the namespace of the component.
    257              Jabberd1.4 and Ejabberd use the default namespace then for all client messages.
    258              Jabberd2 uses jabber:client.
    259              'server' argument is a server name that you are connecting to (f.e. "localhost").
    260              'port' can be specified if 'server' resolves to correct IP. If it is not then you'll need to specify IP
    261              and port while calling "connect()"."""
    262          CommonClient.__init__(self,server,port=port,debug=debug)
    263          self.typ=typ
    264          self.sasl=sasl
    265          self.bind=bind
    266          self.route=route
    267          self.xcp=xcp
    268          if domains:
    269              self.domains=domains
    270          else:
    271              self.domains=[server]
    272
    273 -    def connect(self,server=None,proxy=None):
    274          """ This will connect to the server, and if the features tag is found then set
    275              the namespace to be jabber:client as that is required for jabberd2.
    276              'server' and 'proxy' arguments have the same meaning as in xmpp.Client.connect() """
    277          if self.sasl:
    278              self.Namespace=auth.NS_COMPONENT_1
    279              self.Server=server[0]
    280          CommonClient.connect(self,server=server,proxy=proxy)
    281          if self.connected and (self.typ=='jabberd2' or not self.typ and self.Dispatcher.Stream.features != None) and (not self.xcp):
    282              self.defaultNamespace=auth.NS_CLIENT
    283              self.Dispatcher.RegisterNamespace(self.defaultNamespace)
    284              self.Dispatcher.RegisterProtocol('iq',dispatcher.Iq)
    285              self.Dispatcher.RegisterProtocol('message',dispatcher.Message)
    286              self.Dispatcher.RegisterProtocol('presence',dispatcher.Presence)
    287          return self.connected
    288
    289 -    def dobind(self, sasl):
    290          # This has to be done before binding, because we can receive a route stanza before binding finishes
    291          self._route = self.route
    292          if self.bind:
    293              for domain in self.domains:
    294                  auth.ComponentBind(sasl).PlugIn(self)
    295                  while self.ComponentBind.bound is None: self.Process(1)
    296                  if (not self.ComponentBind.Bind(domain)):
    297                      self.ComponentBind.PlugOut()
    298                      return
    299                  self.ComponentBind.PlugOut()
    300
    301 -    def auth(self,name,password,dup=None):
    302          """ Authenticate component "name" with password "password"."""
    303          self._User,self._Password,self._Resource=name,password,''
    304          try:
    305              if self.sasl: auth.SASL(name,password).PlugIn(self)
    306              if not self.sasl or self.SASL.startsasl=='not-supported':
    307                  if auth.NonSASL(name,password,'').PlugIn(self):
    308                      self.dobind(sasl=False)
    309                      self.connected+='+old_auth'
    310                      return 'old_auth'
    311                  return
    312              self.SASL.auth()
    313              while self.SASL.startsasl=='in-process' and self.Process(1): pass
    314              if self.SASL.startsasl=='success':
    315                  self.dobind(sasl=True)
    316                  self.connected+='+sasl'
    317                  return 'sasl'
    318              else:
    319                  raise auth.NotAuthorized(self.SASL.startsasl)
    320          except:
    321              self.DEBUG(self.DBG,"Failed to authenticate %s"%name,'error')
    322