Package pyxmpp :: Module client |
[hide private]

# Source Code for Module pyxmpp.client

      1  #
      2  # (C) Copyright 2003-2010 Jacek Konieczny <jajcus@jajcus.net>
      3  #
      4  # This program is free software; you can redistribute it and/or modify
      5  # it under the terms of the GNU Lesser General Public License Version
      6  # 2.1 as published by the Free Software Foundation.
      7  #
      8  # This program is distributed in the hope that it will be useful,
      9  # but WITHOUT ANY WARRANTY; without even the implied warranty of
     10  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     11  # GNU Lesser General Public License for more details.
     12  #
     13  # You should have received a copy of the GNU Lesser General Public
     14  # License along with this program; if not, write to the Free Software
     15  # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
     16  #
     17
     18  """Basic XMPP-IM client implementation.
     19
     20  Normative reference:
     21    - `RFC 3921 <http://www.ietf.org/rfc/rfc3921.txt>`__
     22  """
     23
     24  __docformat__="restructuredtext en"
     25
     26  import threading
     27  import logging
     28
     29  from pyxmpp.clientstream import ClientStream
     30  from pyxmpp.iq import Iq
     31  from pyxmpp.presence import Presence
     32  from pyxmpp.roster import Roster
     33  from pyxmpp.exceptions import ClientError, FatalClientError
     34  from pyxmpp.interfaces import IPresenceHandlersProvider, IMessageHandlersProvider
     35  from pyxmpp.interfaces import IIqHandlersProvider, IStanzaHandlersProvider
     36
     37 -class Client:
     38      """Base class for an XMPP-IM client.
     39
     40      This class does not provide any JSF extensions to the XMPP protocol,
     41      including legacy authentication methods.
     42
     43      :Ivariables:
     44          - `jid`: configured JID of the client (current actual JID
     45            is avialable as `self.stream.jid`).
     46          - `password`: authentication password.
     47          - `server`: server to use if non-standard and not discoverable
     48            by SRV lookups.
     49          - `port`: port number on the server to use if non-standard and not
     50            discoverable by SRV lookups.
     51          - `auth_methods`: methods allowed for stream authentication. SASL
     52            mechanism names should be preceded with "sasl:" prefix.
     53          - `keepalive`: keepalive interval for the stream or 0 when keepalive is
     54            disabled.
     55          - `stream`: current stream when the client is connected,
     56            `None` otherwise.
     57          - `roster`: user's roster or `None` if the roster is not yet retrieved.
     58          - `session_established`: `True` when an IM session is established.
     59          - `lock`: lock for synchronizing `Client` attributes access.
     60          - `state_changed`: condition notified the the object state changes
     61            (stream becomes connected, session established etc.).
     62          - `interface_providers`: list of object providing interfaces that
     63            could be used by the Client object. Initialized to [`self`] by
     64            the constructor if not set earlier. Put objects providing
     65            `IPresenceHandlersProvider`, `IMessageHandlersProvider`,
     66            `IIqHandlersProvider` or `IStanzaHandlersProvider` into this list.
     67      :Types:
     68          - `jid`: `pyxmpp.JID`
     69          - `password`: `unicode`
     70          - `server`: `unicode`
     71          - `port`: `int`
     72          - `auth_methods`: `list` of `str`
     73          - `keepalive`: `int`
     74          - `stream`: `pyxmpp.ClientStream`
     75          - `roster`: `pyxmpp.Roster`
     76          - `session_established`: `bool`
     77          - `lock`: `threading.RLock`
     78          - `state_changed`: `threading.Condition`
     79          - `interface_providers`: `list`
     80      """
     81 -    def __init__(self,jid=None,password=None,server=None,port=5222,
     82              auth_methods=("sasl:DIGEST-MD5",),
     83              tls_settings=None,keepalive=0):
     84          """Initialize a Client object.
     85
     86          :Parameters:
     87              - `jid`: user full JID for the connection.
     88              - `password`: user password.
     89              - `server`: server to use. If not given then address will be derived form the JID.
     90              - `port`: port number to use. If not given then address will be derived form the JID.
     91              - `auth_methods`: sallowed authentication methods. SASL authentication mechanisms
     92                in the list should be prefixed with "sasl:" string.
     93              - `tls_settings`: settings for StartTLS -- `TLSSettings` instance.
     94              - `keepalive`: keepalive output interval. 0 to disable.
     95          :Types:
     96              - `jid`: `pyxmpp.JID`
     97              - `password`: `unicode`
     98              - `server`: `unicode`
     99              - `port`: `int`
    100              - `auth_methods`: sequence of `str`
    101              - `tls_settings`: `pyxmpp.TLSSettings`
    102              - `keepalive`: `int`
    103          """
    104          self.jid=jid
    105          self.password=password
    106          self.server=server
    107          self.port=port
    108          self.auth_methods=list(auth_methods)
    109          self.tls_settings=tls_settings
    110          self.keepalive=keepalive
    111          self.stream=None
    112          self.lock=threading.RLock()
    113          self.state_changed=threading.Condition(self.lock)
    114          self.session_established=False
    115          self.roster=None
    116          self.stream_class=ClientStream
    117          if not hasattr(self, "interface_providers"):
    118              self.interface_providers = [self]
    119          self.__logger=logging.getLogger("pyxmpp.Client")
    120
    121  # public methods
    122
    123 -    def connect(self, register = False):
    124          """Connect to the server and set up the stream.
    125
    126          Set `self.stream` and notify `self.state_changed` when connection
    127          succeeds."""
    128          if not self.jid:
    129              raise ClientError, "Cannot connect: no or bad JID given"
    130          self.lock.acquire()
    131          try:
    132              stream = self.stream
    133              self.stream = None
    134              if stream:
    135                  stream.close()
    136
    137              self.__logger.debug("Creating client stream: %r, auth_methods=%r"
    138                      % (self.stream_class, self.auth_methods))
    139              stream=self.stream_class(jid = self.jid,
    140                      password = self.password,
    141                      server = self.server,
    142                      port = self.port,
    143                      auth_methods = self.auth_methods,
    144                      tls_settings = self.tls_settings,
    145                      keepalive = self.keepalive,
    146                      owner = self)
    147              stream.process_stream_error = self.stream_error
    148              self.stream_created(stream)
    149              stream.state_change = self.__stream_state_change
    150              stream.connect()
    151              self.stream = stream
    152              self.state_changed.notify()
    153              self.state_changed.release()
    154          except:
    155              self.stream = None
    156              self.state_changed.release()
    157              raise
    158
    159 -    def get_stream(self):
    160          """Get the connected stream object.
    161
    162          :return: stream object or `None` if the client is not connected.
    163          :returntype: `pyxmpp.ClientStream`"""
    164          self.lock.acquire()
    165          stream=self.stream
    166          self.lock.release()
    167          return stream
    168
    169 -    def disconnect(self):
    170          """Disconnect from the server."""
    171          stream=self.get_stream()
    172          if stream:
    173              stream.disconnect()
    174
    175 -    def request_session(self):
    176          """Request an IM session."""
    177          stream=self.get_stream()
    178          if not stream.version:
    179              need_session=False
    180          elif not stream.features:
    181              need_session=False
    182          else:
    183              ctxt = stream.doc_in.xpathNewContext()
    184              ctxt.setContextNode(stream.features)
    185              ctxt.xpathRegisterNs("sess","urn:ietf:params:xml:ns:xmpp-session")
    186              # jabberd2 hack
    187              ctxt.xpathRegisterNs("jsess","http://jabberd.jabberstudio.org/ns/session/1.0")
    188              sess_n=None
    189              try:
    190                  sess_n=ctxt.xpathEval("sess:session or jsess:session")
    191              finally:
    192                  ctxt.xpathFreeContext()
    193              if sess_n:
    194                  need_session=True
    195              else:
    196                  need_session=False
    197
    198          if not need_session:
    199              self.state_changed.acquire()
    200              self.session_established=1
    201              self.state_changed.notify()
    202              self.state_changed.release()
    203              self._session_started()
    204          else:
    205              iq=Iq(stanza_type="set")
    206              iq.new_query("urn:ietf:params:xml:ns:xmpp-session","session")
    207              stream.set_response_handlers(iq,
    208                  self.__session_result,self.__session_error,self.__session_timeout)
    209              stream.send(iq)
    210
    211 -    def request_roster(self):
    212          """Request the user's roster."""
    213          stream=self.get_stream()
    214          iq=Iq(stanza_type="get")
    215          iq.new_query("jabber:iq:roster")
    216          stream.set_response_handlers(iq,
    217              self.__roster_result,self.__roster_error,self.__roster_timeout)
    218          stream.set_iq_set_handler("query","jabber:iq:roster",self.__roster_push)
    219          stream.send(iq)
    220
    221 -    def get_socket(self):
    222          """Get the socket object of the active connection.
    223
    224          :return: socket used by the stream.
    225          :returntype: `socket.socket`"""
    226          return self.stream.socket
    227
    228 -    def loop(self,timeout=1):
    229          """Simple "main loop" for the client.
    230
    231          By default just call the `pyxmpp.Stream.loop_iter` method of
    232          `self.stream`, which handles stream input and `self.idle` for some
    233          "housekeeping" work until the stream is closed.
    234
    235          This usually will be replaced by something more sophisticated. E.g.
    236          handling of other input sources."""
    237          while 1:
    238              stream=self.get_stream()
    239              if not stream:
    240                  break
    241              act=stream.loop_iter(timeout)
    242              if not act:
    243                  self.idle()
    244
    245  # private methods
    246
    247 -    def __session_timeout(self):
    248          """Process session request time out.
    249
    250          :raise FatalClientError:"""
    251          raise FatalClientError("Timeout while tryin to establish a session")
    252
    253 -    def __session_error(self,iq):
    254          """Process session request failure.
    255
    256          :Parameters:
    257              - `iq`: IQ error stanza received as result of the session request.
    258          :Types:
    259              - `iq`: `pyxmpp.Iq`
    260
    261          :raise FatalClientError:"""
    262          err=iq.get_error()
    263          msg=err.get_message()
    264          raise FatalClientError("Failed to establish a session: "+msg)
    265
    266 -    def __session_result(self, _unused):
    267          """Process session request success.
    268
    269          :Parameters:
    270              - `_unused`: IQ result stanza received in reply to the session request.
    271          :Types:
    272              - `_unused`: `pyxmpp.Iq`"""
    273          self.state_changed.acquire()
    274          self.session_established=True
    275          self.state_changed.notify()
    276          self.state_changed.release()
    277          self._session_started()
    278
    279 -    def _session_started(self):
    280          """Called when session is started.
    281
    282          Activates objects from `self.interface_provides` by installing
    283          their stanza handlers, etc."""
    284          for ob in self.interface_providers:
    285              if IPresenceHandlersProvider.providedBy(ob):
    286                  for handler_data in ob.get_presence_handlers():
    287                      self.stream.set_presence_handler(*handler_data)
    288              if IMessageHandlersProvider.providedBy(ob):
    289                  for handler_data in ob.get_message_handlers():
    290                      self.stream.set_message_handler(*handler_data)
    291              if IIqHandlersProvider.providedBy(ob):
    292                  for handler_data in ob.get_iq_get_handlers():
    293                      self.stream.set_iq_get_handler(*handler_data)
    294                  for handler_data in ob.get_iq_set_handlers():
    295                      self.stream.set_iq_set_handler(*handler_data)
    296          self.session_started()
    297
    298 -    def __roster_timeout(self):
    299          """Process roster request time out.
    300
    301          :raise ClientError:"""
    302          raise ClientError("Timeout while tryin to retrieve roster")
    303
    304 -    def __roster_error(self,iq):
    305          """Process roster request failure.
    306
    307          :Parameters:
    308              - `iq`: IQ error stanza received as result of the roster request.
    309          :Types:
    310              - `iq`: `pyxmpp.Iq`
    311
    312          :raise ClientError:"""
    313          err=iq.get_error()
    314          msg=err.get_message()
    315          raise ClientError("Roster retrieval failed: "+msg)
    316
    317 -    def __roster_result(self,iq):
    318          """Process roster request success.
    319
    320          :Parameters:
    321              - `iq`: IQ result stanza received in reply to the roster request.
    322          :Types:
    323              - `iq`: `pyxmpp.Iq`"""
    324          q=iq.get_query()
    325          if q:
    326              self.state_changed.acquire()
    327              self.roster=Roster(q)
    328              self.state_changed.notify()
    329              self.state_changed.release()
    330              self.roster_updated()
    331          else:
    332              raise ClientError("Roster retrieval failed")
    333
    334 -    def __roster_push(self,iq):
    335          """Process a "roster push" (change notification) received.
    336
    337          :Parameters:
    338              - `iq`: IQ result stanza received.
    339          :Types:
    340              - `iq`: `pyxmpp.Iq`"""
    341          fr=iq.get_from()
    342          if fr and fr != self.jid and fr != self.jid.bare():
    343              resp=iq.make_error_response("forbidden")
    344              self.stream.send(resp)
    345              self.__logger.warning("Got roster update from wrong source")
    346              return
    347          if not self.roster:
    348              raise ClientError("Roster update, but no roster")
    349          q=iq.get_query()
    350          item=self.roster.update(q)
    351          if item:
    352              self.roster_updated(item)
    353          resp=iq.make_result_response()
    354          self.stream.send(resp)
    355
    356 -    def __stream_state_change(self,state,arg):
    357          """Handle stream state changes.
    358
    359          Call apopriate methods of self.
    360
    361          :Parameters:
    362              - `state`: the new state.
    363              - `arg`: state change argument.
    364          :Types:
    365              - `state`: `str`"""
    366          self.stream_state_changed(state,arg)
    367          if state=="fully connected":
    368              self.connected()
    369          elif state=="authorized":
    370              self.authorized()
    371          elif state=="disconnected":
    372              self.state_changed.acquire()
    373              try:
    374                  if self.stream:
    375                      self.stream.close()
    376                  self.stream_closed(self.stream)
    377                  self.stream=None
    378                  self.state_changed.notify()
    379              finally:
    380                  self.state_changed.release()
    381              self.disconnected()
    382
    383  # Method to override
    384 -    def idle(self):
    385          """Do some "housekeeping" work like cache expiration or timeout
    386          handling. Should be called periodically from the application main
    387          loop. May be overriden in derived classes."""
    388          stream=self.get_stream()
    389          if stream:
    390              stream.idle()
    391
    392 -    def stream_created(self,stream):
    393          """Handle stream creation event. May be overriden in derived classes.
    394          This one does nothing.
    395
    396          :Parameters:
    397              - `stream`: the new stream.
    398          :Types:
    399              - `stream`: `pyxmpp.ClientStream`"""
    400          pass
    401
    402 -    def stream_closed(self,stream):
    403          """Handle stream closure event. May be overriden in derived classes.
    404          This one does nothing.
    405
    406          :Parameters:
    407              - `stream`: the new stream.
    408          :Types:
    409              - `stream`: `pyxmpp.ClientStream`"""
    410          pass
    411
    412 -    def session_started(self):
    413          """Handle session started event. May be overriden in derived classes.
    414          This one requests the user's roster and sends the initial presence."""
    415          self.request_roster()
    416          p=Presence()
    417          self.stream.send(p)
    418
    419 -    def stream_error(self,err):
    420          """Handle stream error received. May be overriden in derived classes.
    421          This one passes an error messages to logging facilities.
    422
    423          :Parameters:
    424              - `err`: the error element received.
    425          :Types:
    426              - `err`: `pyxmpp.error.StreamErrorNode`"""
    427          self.__logger.error("Stream error: condition: %s %r"
    428                  % (err.get_condition().name,err.serialize()))
    429
    430 -    def roster_updated(self,item=None):
    431          """Handle roster update event. May be overriden in derived classes.
    432          This one does nothing.
    433
    434          :Parameters:
    435              - `item`: the roster item changed or `None` if whole roster was
    436                received.
    437          :Types:
    438              - `item`: `pyxmpp.RosterItem`"""
    439          pass
    440
    441 -    def stream_state_changed(self,state,arg):
    442          """Handle any stream state change. May be overriden in derived classes.
    443          This one does nothing.
    444
    445          :Parameters:
    446              - `state`: the new state.
    447              - `arg`: state change argument.
    448          :Types:
    449              - `state`: `str`"""
    450          pass
    451
    452 -    def connected(self):
    453          """Handle "connected" event. May be overriden in derived classes.
    454          This one does nothing."""
    455          pass
    456
    457 -    def authenticated(self):
    458          """Handle "authenticated" event. May be overriden in derived classes.
    459          This one does nothing."""
    460          pass
    461
    462 -    def authorized(self):
    463          """Handle "authorized" event. May be overriden in derived classes.
    464          This one requests an IM session."""
    465          self.request_session()
    466
    467 -    def disconnected(self):
    468          """Handle "disconnected" event. May be overriden in derived classes.
    469          This one does nothing."""
    470          pass
    471
    472  # vi: sts=4 et sw=4
    473