Package pyxmpp :: Module stanzaprocessor

[hide private]

# Source Code for Module pyxmpp.stanzaprocessor

      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  """Handling of XMPP stanzas.
     19
     20  Normative reference:
     21    - `RFC 3920 <http://www.ietf.org/rfc/rfc3920.txt>`__
     22  """
     23
     24  __docformat__="restructuredtext en"
     25
     26  import libxml2
     27  import logging
     28  import threading
     29
     30  from pyxmpp.expdict import ExpiringDictionary
     31  from pyxmpp.exceptions import ProtocolError, BadRequestProtocolError, FeatureNotImplementedProtocolError
     32  from pyxmpp.stanza import Stanza
     33
     34 -class StanzaProcessor:
     35      """Universal stanza handler/router class.
     36
     37      Provides facilities to set up custom handlers for various types of stanzas.
     38
     39      :Ivariables:
     40          - `lock`: lock object used to synchronize access to the
     41            `StanzaProcessor` object.
     42          - `me`: local JID.
     43          - `peer`: remote stream endpoint JID.
     44          - `process_all_stanzas`: when `True` then all stanzas received are
     45            considered local.
     46          - `initiator`: `True` if local stream endpoint is the initiating entity.
     47      """
     48 -    def __init__(self):
     49          """Initialize a `StanzaProcessor` object."""
     50          self.me=None
     51          self.peer=None
     52          self.initiator=None
     53          self.peer_authenticated=False
     54          self.process_all_stanzas=True
     55          self._iq_response_handlers=ExpiringDictionary()
     56          self._iq_get_handlers={}
     57          self._iq_set_handlers={}
     58          self._message_handlers=[]
     59          self._presence_handlers=[]
     60          self.__logger=logging.getLogger("pyxmpp.Stream")
     61          self.lock=threading.RLock()
     62
     63 -    def process_response(self, response):
     64          """Examines out the response returned by a stanza handler and sends all
     65          stanzas provided.
     66
     67          :Returns:
     68             - `True`: if `response` is `Stanza`, iterable or `True` (meaning the stanza was processed).
     69             - `False`: when `response` is `False` or `None`
     70          :returntype: `bool`
     71          """
     72
     73          if response is None or response is False:
     74              return False
     75
     76          if isinstance(response, Stanza):
     77              self.send(response)
     78              return True
     79
     80          try:
     81              response = iter(response)
     82          except TypeError:
     83              return bool(response)
     84
     85          for stanza in response:
     86              if isinstance(stanza, Stanza):
     87                  self.send(stanza)
     88          return True
     89
     90 -    def process_iq(self, stanza):
     91          """Process IQ stanza received.
     92
     93          :Parameters:
     94              - `stanza`: the stanza received
     95
     96          If a matching handler is available pass the stanza to it.
     97          Otherwise ignore it if it is "error" or "result" stanza
     98          or return "feature-not-implemented" error."""
     99
    100          sid=stanza.get_id()
    101          fr=stanza.get_from()
    102
    103          typ=stanza.get_type()
    104          if typ in ("result","error"):
    105              if fr:
    106                  ufr=fr.as_unicode()
    107              else:
    108                  ufr=None
    109              res_handler = err_handler = None
    110              try:
    111                  res_handler, err_handler = self._iq_response_handlers.pop((sid,ufr))
    112              except KeyError:
    113                  if ( (fr==self.peer or fr==self.me or fr==self.me.bare()) ):
    114                      try:
    115                          res_handler, err_handler = self._iq_response_handlers.pop((sid,None))
    116                      except KeyError:
    117                          pass
    118                  if None is res_handler is err_handler:
    119                      return False
    120              if typ=="result":
    121                  response = res_handler(stanza)
    122              else:
    123                  response = err_handler(stanza)
    124              self.process_response(response)
    125              return True
    126
    127          q=stanza.get_query()
    128          if not q:
    129              raise BadRequestProtocolError, "Stanza with no child element"
    130          el=q.name
    131          ns=q.ns().getContent()
    132
    133          if typ=="get":
    134              if self._iq_get_handlers.has_key((el,ns)):
    135                  response = self._iq_get_handlers[(el,ns)](stanza)
    136                  self.process_response(response)
    137                  return True
    138              else:
    139                  raise FeatureNotImplementedProtocolError, "Not implemented"
    140          elif typ=="set":
    141              if self._iq_set_handlers.has_key((el,ns)):
    142                  response = self._iq_set_handlers[(el,ns)](stanza)
    143                  self.process_response(response)
    144                  return True
    145              else:
    146                  raise FeatureNotImplementedProtocolError, "Not implemented"
    147          else:
    148              raise BadRequestProtocolError, "Unknown IQ stanza type"
    149
    150 -    def __try_handlers(self,handler_list,typ,stanza):
    151          """ Search the handler list for handlers matching
    152          given stanza type and payload namespace. Run the
    153          handlers found ordering them by priority until
    154          the first one which returns `True`.
    155
    156          :Parameters:
    157              - `handler_list`: list of available handlers
    158              - `typ`: stanza type (value of its "type" attribute)
    159              - `stanza`: the stanza to handle
    160
    161          :return: result of the last handler or `False` if no
    162              handler was found."""
    163          namespaces=[]
    164          if stanza.xmlnode.children:
    165              c=stanza.xmlnode.children
    166              while c:
    167                  try:
    168                      ns=c.ns()
    169                  except libxml2.treeError:
    170                      ns=None
    171                  if ns is None:
    172                      c=c.next
    173                      continue
    174                  ns_uri=ns.getContent()
    175                  if ns_uri not in namespaces:
    176                      namespaces.append(ns_uri)
    177                  c=c.next
    178          for handler_entry in handler_list:
    179              t=handler_entry[1]
    180              ns=handler_entry[2]
    181              handler=handler_entry[3]
    182              if t!=typ:
    183                  continue
    184              if ns is not None and ns not in namespaces:
    185                  continue
    186              response = handler(stanza)
    187              if self.process_response(response):
    188                  return True
    189          return False
    190
    191 -    def process_message(self,stanza):
    192          """Process message stanza.
    193
    194          Pass it to a handler of the stanza's type and payload namespace.
    195          If no handler for the actual stanza type succeeds then hadlers
    196          for type "normal" are used.
    197
    198          :Parameters:
    199              - `stanza`: message stanza to be handled
    200          """
    201
    202          if not self.initiator and not self.peer_authenticated:
    203              self.__logger.debug("Ignoring message - peer not authenticated yet")
    204              return True
    205
    206          typ=stanza.get_type()
    207          if self.__try_handlers(self._message_handlers,typ,stanza):
    208              return True
    209          if typ!="error":
    210              return self.__try_handlers(self._message_handlers,"normal",stanza)
    211          return False
    212
    213 -    def process_presence(self,stanza):
    214          """Process presence stanza.
    215
    216          Pass it to a handler of the stanza's type and payload namespace.
    217
    218          :Parameters:
    219              - `stanza`: presence stanza to be handled
    220          """
    221
    222          if not self.initiator and not self.peer_authenticated:
    223              self.__logger.debug("Ignoring presence - peer not authenticated yet")
    224              return True
    225
    226          typ=stanza.get_type()
    227          if not typ:
    228              typ="available"
    229          return self.__try_handlers(self._presence_handlers,typ,stanza)
    230
    231 -    def route_stanza(self,stanza):
    232          """Process stanza not addressed to us.
    233
    234          Return "recipient-unavailable" return if it is not
    235          "error" nor "result" stanza.
    236
    237          This method should be overriden in derived classes if they
    238          are supposed to handle stanzas not addressed directly to local
    239          stream endpoint.
    240
    241          :Parameters:
    242              - `stanza`: presence stanza to be processed
    243          """
    244          if stanza.get_type() not in ("error","result"):
    245              r = stanza.make_error_response("recipient-unavailable")
    246              self.send(r)
    247          return True
    248
    249 -    def process_stanza(self,stanza):
    250          """Process stanza received from the stream.
    251
    252          First "fix" the stanza with `self.fix_in_stanza()`,
    253          then pass it to `self.route_stanza()` if it is not directed
    254          to `self.me` and `self.process_all_stanzas` is not True. Otherwise
    255          stanza is passwd to `self.process_iq()`, `self.process_message()`
    256          or `self.process_presence()` appropriately.
    257
    258          :Parameters:
    259              - `stanza`: the stanza received.
    260
    261          :returns: `True` when stanza was handled
    262          """
    263
    264          self.fix_in_stanza(stanza)
    265          to=stanza.get_to()
    266
    267          if not self.process_all_stanzas and to and to!=self.me and to.bare()!=self.me.bare():
    268              return self.route_stanza(stanza)
    269
    270          try:
    271              if stanza.stanza_type=="iq":
    272                  if self.process_iq(stanza):
    273                      return True
    274              elif stanza.stanza_type=="message":
    275                  if self.process_message(stanza):
    276                      return True
    277              elif stanza.stanza_type=="presence":
    278                  if self.process_presence(stanza):
    279                      return True
    280          except ProtocolError, e:
    281              typ = stanza.get_type()
    282              if typ != 'error' and (typ != 'result' or stanza.stanza_type != 'iq'):
    283                  r = stanza.make_error_response(e.xmpp_name)
    284                  self.send(r)
    285                  e.log_reported()
    286              else:
    287                  e.log_ignored()
    288
    289          self.__logger.debug("Unhandled %r stanza: %r" % (stanza.stanza_type,stanza.serialize()))
    290          return False
    291
    292 -    def check_to(self,to):
    293          """Check "to" attribute of received stream header.
    294
    295          :return: `to` if it is equal to `self.me`, None otherwise.
    296
    297          Should be overriden in derived classes which require other logic
    298          for handling that attribute."""
    299          if to!=self.me:
    300              return None
    301          return to
    302
    303 -    def set_response_handlers(self,iq,res_handler,err_handler,timeout_handler=None,timeout=300):
    304          """Set response handler for an IQ "get" or "set" stanza.
    305
    306          This should be called before the stanza is sent.
    307
    308          :Parameters:
    309              - `iq`: an IQ stanza
    310              - `res_handler`: result handler for the stanza. Will be called
    311                when matching <iq type="result"/> is received. Its only
    312                argument will be the stanza received. The handler may return
    313                a stanza or list of stanzas which should be sent in response.
    314              - `err_handler`: error handler for the stanza. Will be called
    315                when matching <iq type="error"/> is received. Its only
    316                argument will be the stanza received. The handler may return
    317                a stanza or list of stanzas which should be sent in response
    318                but this feature should rather not be used (it is better not to
    319                respond to 'error' stanzas).
    320              - `timeout_handler`: timeout handler for the stanza. Will be called
    321                when no matching <iq type="result"/> or <iq type="error"/> is
    322                received in next `timeout` seconds. The handler should accept
    323                two arguments and ignore them.
    324              - `timeout`: timeout value for the stanza. After that time if no
    325                matching <iq type="result"/> nor <iq type="error"/> stanza is
    326                received, then timeout_handler (if given) will be called.
    327          """
    328          self.lock.acquire()
    329          try:
    330              self._set_response_handlers(iq,res_handler,err_handler,timeout_handler,timeout)
    331          finally:
    332              self.lock.release()
    333
    334 -    def _set_response_handlers(self,iq,res_handler,err_handler,timeout_handler=None,timeout=300):
    335          """Same as `Stream.set_response_handlers` but assume `self.lock` is acquired."""
    336          self.fix_out_stanza(iq)
    337          to=iq.get_to()
    338          if to:
    339              to=to.as_unicode()
    340          if timeout_handler:
    341              self._iq_response_handlers.set_item((iq.get_id(),to),
    342                      (res_handler,err_handler),
    343                      timeout,timeout_handler)
    344          else:
    345              self._iq_response_handlers.set_item((iq.get_id(),to),
    346                      (res_handler,err_handler),timeout)
    347
    348 -    def set_iq_get_handler(self,element,namespace,handler):
    349          """Set <iq type="get"/> handler.
    350
    351          :Parameters:
    352              - `element`: payload element name
    353              - `namespace`: payload element namespace URI
    354              - `handler`: function to be called when a stanza
    355                with defined element is received. Its only argument
    356                will be the stanza received. The handler may return a stanza or
    357                list of stanzas which should be sent in response.
    358
    359          Only one handler may be defined per one namespaced element.
    360          If a handler for the element was already set it will be lost
    361          after calling this method.
    362          """
    363          self.lock.acquire()
    364          try:
    365              self._iq_get_handlers[(element,namespace)]=handler
    366          finally:
    367              self.lock.release()
    368
    369 -    def unset_iq_get_handler(self,element,namespace):
    370          """Remove <iq type="get"/> handler.
    371
    372          :Parameters:
    373              - `element`: payload element name
    374              - `namespace`: payload element namespace URI
    375          """
    376          self.lock.acquire()
    377          try:
    378              if self._iq_get_handlers.has_key((element,namespace)):
    379                  del self._iq_get_handlers[(element,namespace)]
    380          finally:
    381              self.lock.release()
    382
    383 -    def set_iq_set_handler(self,element,namespace,handler):
    384          """Set <iq type="set"/> handler.
    385
    386          :Parameters:
    387              - `element`: payload element name
    388              - `namespace`: payload element namespace URI
    389              - `handler`: function to be called when a stanza
    390                with defined element is received. Its only argument
    391                will be the stanza received. The handler may return a stanza or
    392                list of stanzas which should be sent in response.
    393
    394
    395          Only one handler may be defined per one namespaced element.
    396          If a handler for the element was already set it will be lost
    397          after calling this method."""
    398          self.lock.acquire()
    399          try:
    400              self._iq_set_handlers[(element,namespace)]=handler
    401          finally:
    402              self.lock.release()
    403
    404 -    def unset_iq_set_handler(self,element,namespace):
    405          """Remove <iq type="set"/> handler.
    406
    407          :Parameters:
    408              - `element`: payload element name.
    409              - `namespace`: payload element namespace URI."""
    410          self.lock.acquire()
    411          try:
    412              if self._iq_set_handlers.has_key((element,namespace)):
    413                  del self._iq_set_handlers[(element,namespace)]
    414          finally:
    415              self.lock.release()
    416
    417 -    def __add_handler(self,handler_list,typ,namespace,priority,handler):
    418          """Add a handler function to a prioritized handler list.
    419
    420          :Parameters:
    421              - `handler_list`: a handler list.
    422              - `typ`: stanza type.
    423              - `namespace`: stanza payload namespace.
    424              - `priority`: handler priority. Must be >=0 and <=100. Handlers
    425                with lower priority list will be tried first."""
    426          if priority<0 or priority>100:
    427              raise ValueError,"Bad handler priority (must be in 0:100)"
    428          handler_list.append((priority,typ,namespace,handler))
    429          handler_list.sort()
    430
    431 -    def set_message_handler(self, typ, handler, namespace=None, priority=100):
    432          """Set a handler for <message/> stanzas.
    433
    434          :Parameters:
    435              - `typ`: message type. `None` will be treated the same as "normal",
    436                and will be the default for unknown types (those that have no
    437                handler associated).
    438              - `namespace`: payload namespace. If `None` that message with any
    439                payload (or even with no payload) will match.
    440              - `priority`: priority value for the handler. Handlers with lower
    441                priority value are tried first.
    442              - `handler`: function to be called when a message stanza
    443                with defined type and payload namespace is received. Its only
    444                argument will be the stanza received. The handler may return a
    445                stanza or list of stanzas which should be sent in response.
    446
    447          Multiple <message /> handlers with the same type/namespace/priority may
    448          be set. Order of calling handlers with the same priority is not defined.
    449          Handlers will be called in priority order until one of them returns True or
    450          any stanza(s) to send (even empty list will do).
    451          """
    452          self.lock.acquire()
    453          try:
    454              if not typ:
    455                  typ = "normal"
    456              self.__add_handler(self._message_handlers,typ,namespace,priority,handler)
    457          finally:
    458              self.lock.release()
    459
    460 -    def set_presence_handler(self,typ,handler,namespace=None,priority=100):
    461          """Set a handler for <presence/> stanzas.
    462
    463          :Parameters:
    464              - `typ`: presence type. "available" will be treated the same as `None`.
    465              - `namespace`: payload namespace. If `None` that presence with any
    466                payload (or even with no payload) will match.
    467              - `priority`: priority value for the handler. Handlers with lower
    468                priority value are tried first.
    469              - `handler`: function to be called when a presence stanza
    470                with defined type and payload namespace is received. Its only
    471                argument will be the stanza received. The handler may return a
    472                stanza or list of stanzas which should be sent in response.
    473
    474          Multiple <presence /> handlers with the same type/namespace/priority may
    475          be set. Order of calling handlers with the same priority is not defined.
    476          Handlers will be called in priority order until one of them returns
    477          True or any stanza(s) to send (even empty list will do).
    478          """
    479          self.lock.acquire()
    480          try:
    481              if not typ:
    482                  typ="available"
    483              self.__add_handler(self._presence_handlers,typ,namespace,priority,handler)
    484          finally:
    485              self.lock.release()
    486
    487 -    def fix_in_stanza(self,stanza):
    488          """Modify incoming stanza before processing it.
    489
    490          This implementation does nothig. It should be overriden in derived
    491          classes if needed."""
    492          pass
    493
    494 -    def fix_out_stanza(self,stanza):
    495          """Modify outgoing stanza before sending into the stream.
    496
    497          This implementation does nothig. It should be overriden in derived
    498          classes if needed."""
    499          pass
    500
    501
    502 -    def send(self,stanza):
    503          """Send a stanza somwhere. This one does nothing. Should be overriden
    504          in derived classes.
    505
    506          :Parameters:
    507              - `stanza`: the stanza to send.
    508          :Types:
    509              - `stanza`: `pyxmpp.stanza.Stanza`"""
    510          raise NotImplementedError,"This method must be overriden in derived classes."""
    511
    512
    513  # vi: sts=4 et sw=4
    514