pyxmpp.jabberd.component

Package pyxmpp :: Package jabberd :: Module component |
[hide private]

# Source Code for Module pyxmpp.jabberd.component

      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  """Jabberd external component interface (jabber:component:accept).
     19
     20  Normative reference:
     21    - `JEP 114 <http://www.jabber.org/jeps/jep-0114.html>`__
     22  """
     23
     24  __revision__="$Id: component.py 714 2010-04-05 10:20:10Z jajcus $"
     25  __docformat__="restructuredtext en"
     26
     27  import threading
     28  import logging
     29
     30  from pyxmpp.jabberd.componentstream import ComponentStream
     31  from pyxmpp.utils import from_utf8
     32  from pyxmpp.jabber.disco import DiscoItems,DiscoInfo,DiscoIdentity
     33  from pyxmpp.stanza import Stanza
     34
     35 -class Component:
     36      """Jabber external component ("jabber:component:accept" protocol) interface
     37      implementation.
     38
     39      Override this class to build your components.
     40
     41      :Ivariables:
     42          - `jid`: component JID (should contain only the domain part).
     43          - `secret`: the authentication secret.
     44          - `server`: server to which the commonent will connect.
     45          - `port`: port number on the server to which the commonent will
     46            connect.
     47          - `keepalive`: keepalive interval for the stream.
     48          - `stream`: the XMPP stream object for the active connection
     49            or `None` if no connection is active.
     50          - `disco_items`: disco items announced by the component. Created
     51            when a stream is connected.
     52          - `disco_info`: disco info announced by the component. Created
     53            when a stream is connected.
     54          - `disco_identity`: disco identity (part of disco info) announced by
     55            the component. Created when a stream is connected.
     56          - `disco_category`: disco category to be used to create
     57            `disco_identity`.
     58          - `disco_type`: disco type to be used to create `disco_identity`.
     59
     60      :Types:
     61          - `jid`:  `pyxmpp.JID`
     62          - `secret`: `unicode`
     63          - `server`: `unicode`
     64          - `port`: `int`
     65          - `keepalive`: `int`
     66          - `stream`: `pyxmpp.jabberd.ComponentStream`
     67          - `disco_items`: `pyxmpp.jabber.DiscoItems`
     68          - `disco_info`: `pyxmpp.jabber.DiscoInfo`
     69          - `disco_identity`: `pyxmpp.jabber.DiscoIdentity`
     70          - `disco_category`: `str`
     71          - `disco_type`: `str`"""
     72 -    def __init__(self, jid=None, secret=None, server=None, port=5347,
     73              disco_name=u"PyXMPP based component", disco_category=u"x-service",
     74              disco_type=u"x-unknown", keepalive=0):
     75          """Initialize a `Component` object.
     76
     77          :Parameters:
     78              - `jid`: component JID (should contain only the domain part).
     79              - `secret`: the authentication secret.
     80              - `server`: server name or address the component should connect.
     81              - `port`: port number on the server where the component should connect.
     82              - `disco_name`: disco identity name to be used in the
     83                disco#info responses.
     84              - `disco_category`: disco identity category to be used in the
     85                disco#info responses.  Use `the categories registered by Jabber Registrar <http://www.jabber.org/registrar/disco-categories.html>`__
     86              - `disco_type`: disco identity type to be used in the component's
     87                disco#info responses.  Use `the types registered by Jabber Registrar <http://www.jabber.org/registrar/disco-categories.html>`__
     88              - `keepalive`: keepalive interval for the stream.
     89
     90          :Types:
     91              - `jid`:  `pyxmpp.JID`
     92              - `secret`: `unicode`
     93              - `server`: `str` or `unicode`
     94              - `port`: `int`
     95              - `disco_name`: `unicode`
     96              - `disco_category`: `unicode`
     97              - `disco_type`: `unicode`
     98              - `keepalive`: `int`"""
     99          self.jid=jid
    100          self.secret=secret
    101          self.server=server
    102          self.port=port
    103          self.keepalive=keepalive
    104          self.stream=None
    105          self.lock=threading.RLock()
    106          self.state_changed=threading.Condition(self.lock)
    107          self.stream_class=ComponentStream
    108          self.disco_items=DiscoItems()
    109          self.disco_info=DiscoInfo()
    110          self.disco_identity=DiscoIdentity(self.disco_info,
    111                              disco_name, disco_category, disco_type)
    112          self.register_feature("stringprep")
    113          self.__logger=logging.getLogger("pyxmpp.jabberd.Component")
    114
    115  # public methods
    116
    117 -    def connect(self):
    118          """Establish a connection with the server.
    119
    120          Set `self.stream` to the `pyxmpp.jabberd.ComponentStream` when
    121          initial connection succeeds.
    122
    123          :raise ValueError: when some of the component properties
    124            (`self.jid`, `self.secret`,`self.server` or `self.port`) are wrong."""
    125          if not self.jid or self.jid.node or self.jid.resource:
    126              raise ValueError,"Cannot connect: no or bad JID given"
    127          if not self.secret:
    128              raise ValueError,"Cannot connect: no secret given"
    129          if not self.server:
    130              raise ValueError,"Cannot connect: no server given"
    131          if not self.port:
    132              raise ValueError,"Cannot connect: no port given"
    133
    134          self.lock.acquire()
    135          try:
    136              stream=self.stream
    137              self.stream=None
    138              if stream:
    139                  stream.close()
    140
    141              self.__logger.debug("Creating component stream: %r" % (self.stream_class,))
    142              stream=self.stream_class(jid = self.jid,
    143                      secret = self.secret,
    144                      server = self.server,
    145                      port = self.port,
    146                      keepalive = self.keepalive,
    147                      owner = self)
    148              stream.process_stream_error=self.stream_error
    149              self.stream_created(stream)
    150              stream.state_change=self.__stream_state_change
    151              stream.connect()
    152              self.stream=stream
    153              self.state_changed.notify()
    154              self.state_changed.release()
    155          except:
    156              self.stream=None
    157              self.state_changed.release()
    158              raise
    159
    160 -    def get_stream(self):
    161          """Get the stream of the component in a safe way.
    162
    163          :return: Stream object for the component or `None` if no connection is
    164              active.
    165          :returntype: `pyxmpp.jabberd.ComponentStream`"""
    166          self.lock.acquire()
    167          stream=self.stream
    168          self.lock.release()
    169          return stream
    170
    171 -    def disconnect(self):
    172          """Disconnect from the server."""
    173          stream=self.get_stream()
    174          if stream:
    175              stream.disconnect()
    176
    177 -    def socket(self):
    178          """Get the socket of the connection to the server.
    179
    180          :return: the socket.
    181          :returntype: `socket.socket`"""
    182          return self.stream.socket
    183
    184 -    def loop(self,timeout=1):
    185          """Simple 'main loop' for a component.
    186
    187          This usually will be replaced by something more sophisticated. E.g.
    188          handling of other input sources."""
    189          self.stream.loop(timeout)
    190
    191 -    def register_feature(self, feature_name):
    192          """Register a feature to be announced by Service Discovery.
    193
    194          :Parameters:
    195              - `feature_name`: feature namespace or name.
    196          :Types:
    197              - `feature_name`: `unicode`"""
    198          self.disco_info.add_feature(feature_name)
    199
    200 -    def unregister_feature(self, feature_name):
    201          """Unregister a feature to be announced by Service Discovery.
    202
    203          :Parameters:
    204              - `feature_name`: feature namespace or name.
    205          :Types:
    206              - `feature_name`: `unicode`"""
    207          self.disco_info.remove_feature(feature_name)
    208
    209
    210  # private methods
    211 -    def __stream_state_change(self,state,arg):
    212          """Handle various stream state changes and call right
    213          methods of `self`.
    214
    215          :Parameters:
    216              - `state`: state name.
    217              - `arg`: state parameter.
    218          :Types:
    219              - `state`: `string`
    220              - `arg`: any object"""
    221          self.stream_state_changed(state,arg)
    222          if state=="fully connected":
    223              self.connected()
    224          elif state=="authenticated":
    225              self.authenticated()
    226          elif state=="authorized":
    227              self.authorized()
    228          elif state=="disconnected":
    229              self.state_changed.acquire()
    230              try:
    231                  if self.stream:
    232                      self.stream.close()
    233                  self.stream_closed(self.stream)
    234                  self.stream=None
    235                  self.state_changed.notify()
    236              finally:
    237                  self.state_changed.release()
    238              self.disconnected()
    239
    240 -    def __disco_info(self,iq):
    241          """Handle a disco-info query.
    242
    243          :Parameters:
    244              - `iq`: the stanza received.
    245
    246          Types:
    247              - `iq`: `pyxmpp.Iq`"""
    248          q=iq.get_query()
    249          if q.hasProp("node"):
    250              node=from_utf8(q.prop("node"))
    251          else:
    252              node=None
    253          info=self.disco_get_info(node,iq)
    254          if isinstance(info,DiscoInfo):
    255              resp=iq.make_result_response()
    256              self.__logger.debug("Disco-info query: %s preparing response: %s with reply: %s"
    257                  % (iq.serialize(),resp.serialize(),info.xmlnode.serialize()))
    258              resp.set_content(info.xmlnode.copyNode(1))
    259          elif isinstance(info,Stanza):
    260              resp=info
    261          else:
    262              resp=iq.make_error_response("item-not-found")
    263          self.__logger.debug("Disco-info response: %s" % (resp.serialize(),))
    264          self.stream.send(resp)
    265
    266 -    def __disco_items(self,iq):
    267          """Handle a disco-items query.
    268
    269          :Parameters:
    270              - `iq`: the stanza received.
    271
    272          Types:
    273              - `iq`: `pyxmpp.Iq`"""
    274          q=iq.get_query()
    275          if q.hasProp("node"):
    276              node=from_utf8(q.prop("node"))
    277          else:
    278              node=None
    279          items=self.disco_get_items(node,iq)
    280          if isinstance(items,DiscoItems):
    281              resp=iq.make_result_response()
    282              self.__logger.debug("Disco-items query: %s preparing response: %s with reply: %s"
    283                  % (iq.serialize(),resp.serialize(),items.xmlnode.serialize()))
    284              resp.set_content(items.xmlnode.copyNode(1))
    285          elif isinstance(items,Stanza):
    286              resp=items
    287          else:
    288              resp=iq.make_error_response("item-not-found")
    289          self.__logger.debug("Disco-items response: %s" % (resp.serialize(),))
    290          self.stream.send(resp)
    291
    292  # Method to override
    293 -    def idle(self):
    294          """Do some "housekeeping" work like <iq/> result expiration. Should be
    295          called on a regular basis, usually when the component is idle."""
    296          stream=self.get_stream()
    297          if stream:
    298              stream.idle()
    299
    300 -    def stream_created(self,stream):
    301          """Handle stream creation event.
    302
    303          [may be overriden in derived classes]
    304
    305          By default: do nothing.
    306
    307          :Parameters:
    308              - `stream`: the stream just created.
    309          :Types:
    310              - `stream`: `pyxmpp.jabberd.ComponentStream`"""
    311          pass
    312
    313 -    def stream_closed(self,stream):
    314          """Handle stream closure event.
    315
    316          [may be overriden in derived classes]
    317
    318          By default: do nothing.
    319
    320          :Parameters:
    321              - `stream`: the stream just created.
    322          :Types:
    323              - `stream`: `pyxmpp.jabberd.ComponentStream`"""
    324          pass
    325
    326 -    def stream_error(self,err):
    327          """Handle a stream error received.
    328
    329          [may be overriden in derived classes]
    330
    331          By default: just log it. The stream will be closed anyway.
    332
    333          :Parameters:
    334              - `err`: the error element received.
    335          :Types:
    336              - `err`: `pyxmpp.error.StreamErrorNode`"""
    337          self.__logger.debug("Stream error: condition: %s %r"
    338                  % (err.get_condition().name,err.serialize()))
    339
    340 -    def stream_state_changed(self,state,arg):
    341          """Handle a stream state change.
    342
    343          [may be overriden in derived classes]
    344
    345          By default: do nothing.
    346
    347          :Parameters:
    348              - `state`: state name.
    349              - `arg`: state parameter.
    350          :Types:
    351              - `state`: `string`
    352              - `arg`: any object"""
    353          pass
    354
    355 -    def connected(self):
    356          """Handle stream connection event.
    357
    358          [may be overriden in derived classes]
    359
    360          By default: do nothing."""
    361          pass
    362
    363 -    def authenticated(self):
    364          """Handle successful authentication event.
    365
    366          A good place to register stanza handlers and disco features.
    367
    368          [should be overriden in derived classes]
    369
    370          By default: set disco#info and disco#items handlers."""
    371          self.__logger.debug("Setting up Disco handlers...")
    372          self.stream.set_iq_get_handler("query","http://jabber.org/protocol/disco#items",
    373                                      self.__disco_items)
    374          self.stream.set_iq_get_handler("query","http://jabber.org/protocol/disco#info",
    375                                      self.__disco_info)
    376
    377 -    def authorized(self):
    378          """Handle successful authorization event."""
    379          pass
    380
    381 -    def disco_get_info(self,node,iq):
    382          """Get disco#info data for a node.
    383
    384          [may be overriden in derived classes]
    385
    386          By default: return `self.disco_info` if no specific node name
    387          is provided.
    388
    389          :Parameters:
    390              - `node`: name of the node queried.
    391              - `iq`: the stanza received.
    392          :Types:
    393              - `node`: `unicode`
    394              - `iq`: `pyxmpp.Iq`"""
    395          to=iq.get_to()
    396          if to and to!=self.jid:
    397              return iq.make_error_response("recipient-unavailable")
    398          if not node and self.disco_info:
    399              return self.disco_info
    400          return None
    401
    402 -    def disco_get_items(self,node,iq):
    403          """Get disco#items data for a node.
    404
    405          [may be overriden in derived classes]
    406
    407          By default: return `self.disco_items` if no specific node name
    408          is provided.
    409
    410          :Parameters:
    411              - `node`: name of the node queried.
    412              - `iq`: the stanza received.
    413          :Types:
    414              - `node`: `unicode`
    415              - `iq`: `pyxmpp.Iq`"""
    416          to=iq.get_to()
    417          if to and to!=self.jid:
    418              return iq.make_error_response("recipient-unavailable")
    419          if not node and self.disco_items:
    420              return self.disco_items
    421          return None
    422
    423 -    def disconnected(self):
    424          """Handle stream disconnection (connection closed by peer) event.
    425
    426          [may be overriden in derived classes]
    427
    428          By default: do nothing."""
    429          pass
    430
    431  # vi: sts=4 et sw=4
    432