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