pyxmpp.stanzaprocessor

Package pyxmpp :: Module stanzaprocessor |
[hide private]

# Source Code for Module pyxmpp.stanzaprocessor

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