pyxmpp.jabber.client

Package pyxmpp :: Package jabber :: Module client |
[hide private]

# Source Code for Module pyxmpp.jabber.client

      1  # (C) Copyright 2003-2010 Jacek Konieczny <jajcus@jajcus.net>
      2  #
      3  # This program is free software; you can redistribute it and/or modify
      4  # it under the terms of the GNU Lesser General Public License Version
      5  # 2.1 as published by the Free Software Foundation.
      6  #
      7  # This program is distributed in the hope that it will be useful,
      8  # but WITHOUT ANY WARRANTY; without even the implied warranty of
      9  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     10  # GNU Lesser General Public License for more details.
     11  #
     12  # You should have received a copy of the GNU Lesser General Public
     13  # License along with this program; if not, write to the Free Software
     14  # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
     15  #
     16  """Basic Jabber client functionality implementation.
     17
     18  Extends `pyxmpp.client` interface with legacy authentication
     19  and basic Service Discovery handling.
     20
     21  Normative reference:
     22    - `JEP 78 <http://www.jabber.org/jeps/jep-0078.html>`__
     23    - `JEP 30 <http://www.jabber.org/jeps/jep-0030.html>`__
     24  """
     25
     26  __revision__="$Id: client.py 714 2010-04-05 10:20:10Z jajcus $"
     27  __docformat__="restructuredtext en"
     28
     29  import logging
     30
     31  from pyxmpp.jabber.clientstream import LegacyClientStream
     32  from pyxmpp.jabber.disco import DISCO_ITEMS_NS,DISCO_INFO_NS
     33  from pyxmpp.jabber.disco import DiscoInfo,DiscoItems,DiscoIdentity
     34  from pyxmpp.jabber import disco
     35  from pyxmpp.client import Client
     36  from pyxmpp.stanza import Stanza
     37  from pyxmpp.cache import CacheSuite
     38  from pyxmpp.utils import from_utf8
     39  from pyxmpp.interfaces import IFeaturesProvider
     40
     41 -class JabberClient(Client):
     42      """Base class for a Jabber client.
     43
     44      :Ivariables:
     45          - `disco_items`: default Disco#items reply for a query to an empty node.
     46          - `disco_info`: default Disco#info reply for a query to an empty node --
     47            provides information about the client and its supported fetures.
     48          - `disco_identity`: default identity of the default `disco_info`.
     49          - `register`: when `True` than registration will be started instead of authentication.
     50      :Types:
     51          - `disco_items`: `DiscoItems`
     52          - `disco_info`: `DiscoInfo`
     53          - `register`: `bool`
     54      """
     55 -    def __init__(self,jid=None, password=None, server=None, port=5222,
     56              auth_methods=("sasl:DIGEST-MD5","digest"),
     57              tls_settings=None, keepalive=0,
     58              disco_name=u"pyxmpp based Jabber client", disco_category=u"client",
     59              disco_type=u"pc"):
     60          """Initialize a JabberClient object.
     61
     62          :Parameters:
     63              - `jid`: user full JID for the connection.
     64              - `password`: user password.
     65              - `server`: server to use. If not given then address will be derived form the JID.
     66              - `port`: port number to use. If not given then address will be derived form the JID.
     67              - `auth_methods`: sallowed authentication methods. SASL authentication mechanisms
     68                in the list should be prefixed with "sasl:" string.
     69              - `tls_settings`: settings for StartTLS -- `TLSSettings` instance.
     70              - `keepalive`: keepalive output interval. 0 to disable.
     71              - `disco_name`: name of the client identity in the disco#info
     72                replies.
     73              - `disco_category`: category of the client identity in the disco#info
     74                replies. The default of u'client' should be the right choice in
     75                most cases.
     76              - `disco_type`: type of the client identity in the disco#info
     77                replies. Use `the types registered by Jabber Registrar <http://www.jabber.org/registrar/disco-categories.html>`__
     78          :Types:
     79              - `jid`: `pyxmpp.JID`
     80              - `password`: `unicode`
     81              - `server`: `unicode`
     82              - `port`: `int`
     83              - `auth_methods`: sequence of `str`
     84              - `tls_settings`: `pyxmpp.TLSSettings`
     85              - `keepalive`: `int`
     86              - `disco_name`: `unicode`
     87              - `disco_category`: `unicode`
     88              - `disco_type`: `unicode`
     89          """
     90
     91          Client.__init__(self,jid,password,server,port,auth_methods,tls_settings,keepalive)
     92          self.stream_class = LegacyClientStream
     93          self.disco_items=DiscoItems()
     94          self.disco_info=DiscoInfo()
     95          self.disco_identity=DiscoIdentity(self.disco_info,
     96                              disco_name, disco_category, disco_type)
     97          self.register_feature(u"dnssrv")
     98          self.register_feature(u"stringprep")
     99          self.register_feature(u"urn:ietf:params:xml:ns:xmpp-sasl#c2s")
    100          self.cache = CacheSuite(max_items = 1000)
    101          self.__logger = logging.getLogger("pyxmpp.jabber.JabberClient")
    102
    103  # public methods
    104
    105 -    def connect(self, register = False):
    106          """Connect to the server and set up the stream.
    107
    108          Set `self.stream` and notify `self.state_changed` when connection
    109          succeeds. Additionally, initialize Disco items and info of the client.
    110          """
    111          Client.connect(self, register)
    112          if register:
    113              self.stream.registration_callback = self.process_registration_form
    114
    115 -    def register_feature(self, feature_name):
    116          """Register a feature to be announced by Service Discovery.
    117
    118          :Parameters:
    119              - `feature_name`: feature namespace or name.
    120          :Types:
    121              - `feature_name`: `unicode`"""
    122          self.disco_info.add_feature(feature_name)
    123
    124 -    def unregister_feature(self, feature_name):
    125          """Unregister a feature to be announced by Service Discovery.
    126
    127          :Parameters:
    128              - `feature_name`: feature namespace or name.
    129          :Types:
    130              - `feature_name`: `unicode`"""
    131          self.disco_info.remove_feature(feature_name)
    132
    133 -    def submit_registration_form(self, form):
    134          """Submit a registration form
    135
    136          :Parameters:
    137              - `form`: the form to submit
    138          :Types:
    139              - `form`: `pyxmpp.jabber.dataforms.Form`"""
    140          self.stream.submit_registration_form(form)
    141
    142  # private methods
    143 -    def __disco_info(self,iq):
    144          """Handle a disco#info request.
    145
    146          `self.disco_get_info` method will be used to prepare the query response.
    147
    148          :Parameters:
    149              - `iq`: the IQ stanza received.
    150          :Types:
    151              - `iq`: `pyxmpp.iq.Iq`"""
    152          q=iq.get_query()
    153          if q.hasProp("node"):
    154              node=from_utf8(q.prop("node"))
    155          else:
    156              node=None
    157          info=self.disco_get_info(node,iq)
    158          if isinstance(info,DiscoInfo):
    159              resp=iq.make_result_response()
    160              self.__logger.debug("Disco-info query: %s preparing response: %s with reply: %s"
    161                  % (iq.serialize(),resp.serialize(),info.xmlnode.serialize()))
    162              resp.set_content(info.xmlnode.copyNode(1))
    163          elif isinstance(info,Stanza):
    164              resp=info
    165          else:
    166              resp=iq.make_error_response("item-not-found")
    167          self.__logger.debug("Disco-info response: %s" % (resp.serialize(),))
    168          self.stream.send(resp)
    169
    170 -    def __disco_items(self,iq):
    171          """Handle a disco#items request.
    172
    173          `self.disco_get_items` method will be used to prepare the query response.
    174
    175          :Parameters:
    176              - `iq`: the IQ stanza received.
    177          :Types:
    178              - `iq`: `pyxmpp.iq.Iq`"""
    179          q=iq.get_query()
    180          if q.hasProp("node"):
    181              node=from_utf8(q.prop("node"))
    182          else:
    183              node=None
    184          items=self.disco_get_items(node,iq)
    185          if isinstance(items,DiscoItems):
    186              resp=iq.make_result_response()
    187              self.__logger.debug("Disco-items query: %s preparing response: %s with reply: %s"
    188                  % (iq.serialize(),resp.serialize(),items.xmlnode.serialize()))
    189              resp.set_content(items.xmlnode.copyNode(1))
    190          elif isinstance(items,Stanza):
    191              resp=items
    192          else:
    193              resp=iq.make_error_response("item-not-found")
    194          self.__logger.debug("Disco-items response: %s" % (resp.serialize(),))
    195          self.stream.send(resp)
    196
    197 -    def _session_started(self):
    198          """Called when session is started.
    199
    200          Activates objects from `self.interface_provides` by installing
    201          their disco features."""
    202          Client._session_started(self)
    203          for ob in self.interface_providers:
    204              if IFeaturesProvider.providedBy(ob):
    205                  for ns in ob.get_features():
    206                      self.register_feature(ns)
    207
    208  # methods to override
    209
    210 -    def authorized(self):
    211          """Handle "authorized" event. May be overriden in derived classes.
    212          By default: request an IM session and setup Disco handlers."""
    213          Client.authorized(self)
    214          self.stream.set_iq_get_handler("query",DISCO_ITEMS_NS,self.__disco_items)
    215          self.stream.set_iq_get_handler("query",DISCO_INFO_NS,self.__disco_info)
    216          disco.register_disco_cache_fetchers(self.cache,self.stream)
    217
    218 -    def disco_get_info(self,node,iq):
    219          """Return Disco#info data for a node.
    220
    221          :Parameters:
    222              - `node`: the node queried.
    223              - `iq`: the request stanza received.
    224          :Types:
    225              - `node`: `unicode`
    226              - `iq`: `pyxmpp.iq.Iq`
    227
    228          :return: self.disco_info if `node` is empty or `None` otherwise.
    229          :returntype: `DiscoInfo`"""
    230          to=iq.get_to()
    231          if to and to!=self.jid:
    232              return iq.make_error_response("recipient-unavailable")
    233          if not node and self.disco_info:
    234              return self.disco_info
    235          return None
    236
    237 -    def disco_get_items(self,node,iq):
    238          """Return Disco#items data for a node.
    239
    240          :Parameters:
    241              - `node`: the node queried.
    242              - `iq`: the request stanza received.
    243          :Types:
    244              - `node`: `unicode`
    245              - `iq`: `pyxmpp.iq.Iq`
    246
    247          :return: self.disco_info if `node` is empty or `None` otherwise.
    248          :returntype: `DiscoInfo`"""
    249          to=iq.get_to()
    250          if to and to!=self.jid:
    251              return iq.make_error_response("recipient-unavailable")
    252          if not node and self.disco_items:
    253              return self.disco_items
    254          return None
    255
    256 -    def process_registration_form(self, stanza, form):
    257          """Fill-in the registration form provided by the server.
    258
    259          This default implementation fills-in "username" and "passwords"
    260          fields only and instantly submits the form.
    261
    262          :Parameters:
    263              - `stanza`: the stanza received.
    264              - `form`: the registration form.
    265          :Types:
    266              - `stanza`: `pyxmpp.iq.Iq`
    267              - `form`: `pyxmpp.jabber.dataforms.Form`
    268          """
    269          _unused = stanza
    270          self.__logger.debug(u"default registration callback started. auto-filling-in the form...")
    271          if not 'FORM_TYPE' in form or 'jabber:iq:register' not in form['FORM_TYPE'].values:
    272              raise RuntimeError, "Unknown form type: %r %r" % (form, form['FORM_TYPE'])
    273          for field in form:
    274              if field.name == u"username":
    275                  self.__logger.debug(u"Setting username to %r" % (self.jid.node,))
    276                  field.value = self.jid.node
    277              elif field.name == u"password":
    278                  self.__logger.debug(u"Setting password to %r" % (self.password,))
    279                  field.value = self.password
    280              elif field.required:
    281                  self.__logger.debug(u"Unknown required field: %r" % (field.name,))
    282                  raise RuntimeError, "Unsupported required registration form field %r" % (field.name,)
    283              else:
    284                  self.__logger.debug(u"Unknown field: %r" % (field.name,))
    285          self.submit_registration_form(form)
    286
    287  # vi: sts=4 et sw=4
    288