Package pyxmpp :: Package jabber :: Module clientstream

[hide private]

# Source Code for Module pyxmpp.jabber.clientstream

      1  #
      2  # (C) Copyright 2003-2006 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  """XMPP stream support with fallback to legacy non-SASL Jabber authentication.
     18
     19  Normative reference:
     20    - `JEP 78 <http://www.jabber.org/jeps/jep-0078.html>`__
     21  """
     22
     23  __revision__="$Id: clientstream.py 683 2008-12-05 18:25:45Z jajcus $"
     24  __docformat__="restructuredtext en"
     25
     26  try:
     27      import hashlib
     28      sha_factory = hashlib.sha1
     29  except ImportError:
     30      import sha
     31      sha_factory = sha.new
     32
     33  import logging
     34
     35  from pyxmpp.iq import Iq
     36  from pyxmpp.utils import to_utf8,from_utf8
     37  from pyxmpp.jid import JID
     38  from pyxmpp.clientstream import ClientStream
     39  from pyxmpp.jabber.register import Register
     40
     41  from pyxmpp.exceptions import ClientStreamError, LegacyAuthenticationError, RegistrationError
     42
     43 -class LegacyClientStream(ClientStream):
     44      """Handles Jabber (both XMPP and legacy protocol) client connection stream.
     45
     46      Both client and server side of the connection is supported. This class handles
     47      client SASL and legacy authentication, authorisation and XMPP resource binding.
     48      """
     49 -    def __init__(self, jid, password = None, server = None, port = 5222,
     50              auth_methods = ("sasl:DIGEST-MD5", "digest"),
     51              tls_settings = None, keepalive = 0, owner = None):
     52          """Initialize a LegacyClientStream object.
     53
     54          :Parameters:
     55              - `jid`: local JID.
     56              - `password`: user's password.
     57              - `server`: server to use. If not given then address will be derived form the JID.
     58              - `port`: port number to use. If not given then address will be derived form the JID.
     59              - `auth_methods`: sallowed authentication methods. SASL authentication mechanisms
     60                in the list should be prefixed with "sasl:" string.
     61              - `tls_settings`: settings for StartTLS -- `TLSSettings` instance.
     62              - `keepalive`: keepalive output interval. 0 to disable.
     63              - `owner`: `Client`, `Component` or similar object "owning" this stream.
     64          :Types:
     65              - `jid`: `pyxmpp.JID`
     66              - `password`: `unicode`
     67              - `server`: `unicode`
     68              - `port`: `int`
     69              - `auth_methods`: sequence of `str`
     70              - `tls_settings`: `pyxmpp.TLSSettings`
     71              - `keepalive`: `int`
     72          """
     73          (self.authenticated, self.available_auth_methods, self.auth_stanza,
     74                  self.peer_authenticated, self.auth_method_used,
     75                  self.registration_callback, self.registration_form, self.__register) = (None,) * 8
     76          ClientStream.__init__(self, jid, password, server, port,
     77                              auth_methods, tls_settings, keepalive, owner)
     78          self.__logger=logging.getLogger("pyxmpp.jabber.LegacyClientStream")
     79
     80 -    def _reset(self):
     81          """Reset the `LegacyClientStream` object state, making the object ready
     82          to handle new connections."""
     83          ClientStream._reset(self)
     84          self.available_auth_methods = None
     85          self.auth_stanza = None
     86          self.registration_callback = None
     87
     88 -    def _post_connect(self):
     89          """Initialize authentication when the connection is established
     90          and we are the initiator."""
     91          if not self.initiator:
     92              if "plain" in self.auth_methods or "digest" in self.auth_methods:
     93                  self.set_iq_get_handler("query","jabber:iq:auth",
     94                              self.auth_in_stage1)
     95                  self.set_iq_set_handler("query","jabber:iq:auth",
     96                              self.auth_in_stage2)
     97          elif self.registration_callback:
     98              iq = Iq(stanza_type = "get")
     99              iq.set_content(Register())
    100              self.set_response_handlers(iq, self.registration_form_received, self.registration_error)
    101              self.send(iq)
    102              return
    103          ClientStream._post_connect(self)
    104
    105 -    def _post_auth(self):
    106          """Unregister legacy authentication handlers after successfull
    107          authentication."""
    108          ClientStream._post_auth(self)
    109          if not self.initiator:
    110              self.unset_iq_get_handler("query","jabber:iq:auth")
    111              self.unset_iq_set_handler("query","jabber:iq:auth")
    112
    113 -    def _try_auth(self):
    114          """Try to authenticate using the first one of allowed authentication
    115          methods left.
    116
    117          [client only]"""
    118          if self.authenticated:
    119              self.__logger.debug("try_auth: already authenticated")
    120              return
    121          self.__logger.debug("trying auth: %r" % (self._auth_methods_left,))
    122          if not self._auth_methods_left:
    123              raise LegacyAuthenticationError,"No allowed authentication methods available"
    124          method=self._auth_methods_left[0]
    125          if method.startswith("sasl:"):
    126              return ClientStream._try_auth(self)
    127          elif method not in ("plain","digest"):
    128              self._auth_methods_left.pop(0)
    129              self.__logger.debug("Skipping unknown auth method: %s" % method)
    130              return self._try_auth()
    131          elif self.available_auth_methods is not None:
    132              if method in self.available_auth_methods:
    133                  self._auth_methods_left.pop(0)
    134                  self.auth_method_used=method
    135                  if method=="digest":
    136                      self._digest_auth_stage2(self.auth_stanza)
    137                  else:
    138                      self._plain_auth_stage2(self.auth_stanza)
    139                  self.auth_stanza=None
    140                  return
    141              else:
    142                  self.__logger.debug("Skipping unavailable auth method: %s" % method)
    143          else:
    144              self._auth_stage1()
    145
    146 -    def auth_in_stage1(self,stanza):
    147          """Handle the first stage (<iq type='get'/>) of legacy ("plain" or
    148          "digest") authentication.
    149
    150          [server only]"""
    151          self.lock.acquire()
    152          try:
    153              if "plain" not in self.auth_methods and "digest" not in self.auth_methods:
    154                  iq=stanza.make_error_response("not-allowed")
    155                  self.send(iq)
    156                  return
    157
    158              iq=stanza.make_result_response()
    159              q=iq.new_query("jabber:iq:auth")
    160              q.newChild(None,"username",None)
    161              q.newChild(None,"resource",None)
    162              if "plain" in self.auth_methods:
    163                  q.newChild(None,"password",None)
    164              if "digest" in self.auth_methods:
    165                  q.newChild(None,"digest",None)
    166              self.send(iq)
    167              iq.free()
    168          finally:
    169              self.lock.release()
    170
    171 -    def auth_in_stage2(self,stanza):
    172          """Handle the second stage (<iq type='set'/>) of legacy ("plain" or
    173          "digest") authentication.
    174
    175          [server only]"""
    176          self.lock.acquire()
    177          try:
    178              if "plain" not in self.auth_methods and "digest" not in self.auth_methods:
    179                  iq=stanza.make_error_response("not-allowed")
    180                  self.send(iq)
    181                  return
    182
    183              username=stanza.xpath_eval("a:query/a:username",{"a":"jabber:iq:auth"})
    184              if username:
    185                  username=from_utf8(username[0].getContent())
    186              resource=stanza.xpath_eval("a:query/a:resource",{"a":"jabber:iq:auth"})
    187              if resource:
    188                  resource=from_utf8(resource[0].getContent())
    189              if not username or not resource:
    190                  self.__logger.debug("No username or resource found in auth request")
    191                  iq=stanza.make_error_response("bad-request")
    192                  self.send(iq)
    193                  return
    194
    195              if stanza.xpath_eval("a:query/a:password",{"a":"jabber:iq:auth"}):
    196                  if "plain" not in self.auth_methods:
    197                      iq=stanza.make_error_response("not-allowed")
    198                      self.send(iq)
    199                      return
    200                  else:
    201                      return self._plain_auth_in_stage2(username,resource,stanza)
    202              if stanza.xpath_eval("a:query/a:digest",{"a":"jabber:iq:auth"}):
    203                  if "plain" not in self.auth_methods:
    204                      iq=stanza.make_error_response("not-allowed")
    205                      self.send(iq)
    206                      return
    207                  else:
    208                      return self._digest_auth_in_stage2(username,resource,stanza)
    209          finally:
    210              self.lock.release()
    211
    212 -    def _auth_stage1(self):
    213          """Do the first stage (<iq type='get'/>) of legacy ("plain" or
    214          "digest") authentication.
    215
    216          [client only]"""
    217          iq=Iq(stanza_type="get")
    218          q=iq.new_query("jabber:iq:auth")
    219          q.newTextChild(None,"username",to_utf8(self.my_jid.node))
    220          q.newTextChild(None,"resource",to_utf8(self.my_jid.resource))
    221          self.send(iq)
    222          self.set_response_handlers(iq,self.auth_stage2,self.auth_error,
    223                              self.auth_timeout,timeout=60)
    224          iq.free()
    225
    226 -    def auth_timeout(self):
    227          """Handle legacy authentication timeout.
    228
    229          [client only]"""
    230          self.lock.acquire()
    231          try:
    232              self.__logger.debug("Timeout while waiting for jabber:iq:auth result")
    233              if self._auth_methods_left:
    234                  self._auth_methods_left.pop(0)
    235          finally:
    236              self.lock.release()
    237
    238 -    def auth_error(self,stanza):
    239          """Handle legacy authentication error.
    240
    241          [client only]"""
    242          self.lock.acquire()
    243          try:
    244              err=stanza.get_error()
    245              ae=err.xpath_eval("e:*",{"e":"jabber:iq:auth:error"})
    246              if ae:
    247                  ae=ae[0].name
    248              else:
    249                  ae=err.get_condition().name
    250              raise LegacyAuthenticationError,("Authentication error condition: %s"
    251                          % (ae,))
    252          finally:
    253              self.lock.release()
    254
    255 -    def auth_stage2(self,stanza):
    256          """Handle the first stage authentication response (result of the <iq
    257          type="get"/>).
    258
    259          [client only]"""
    260          self.lock.acquire()
    261          try:
    262              self.__logger.debug("Procesing auth response...")
    263              self.available_auth_methods=[]
    264              if (stanza.xpath_eval("a:query/a:digest",{"a":"jabber:iq:auth"}) and self.stream_id):
    265                  self.available_auth_methods.append("digest")
    266              if (stanza.xpath_eval("a:query/a:password",{"a":"jabber:iq:auth"})):
    267                  self.available_auth_methods.append("plain")
    268              self.auth_stanza=stanza.copy()
    269              self._try_auth()
    270          finally:
    271              self.lock.release()
    272
    273 -    def _plain_auth_stage2(self, _unused):
    274          """Do the second stage (<iq type='set'/>) of legacy "plain"
    275          authentication.
    276
    277          [client only]"""
    278          iq=Iq(stanza_type="set")
    279          q=iq.new_query("jabber:iq:auth")
    280          q.newTextChild(None,"username",to_utf8(self.my_jid.node))
    281          q.newTextChild(None,"resource",to_utf8(self.my_jid.resource))
    282          q.newTextChild(None,"password",to_utf8(self.password))
    283          self.send(iq)
    284          self.set_response_handlers(iq,self.auth_finish,self.auth_error)
    285          iq.free()
    286
    287 -    def _plain_auth_in_stage2(self, username, _unused, stanza):
    288          """Handle the second stage (<iq type='set'/>) of legacy "plain"
    289          authentication.
    290
    291          [server only]"""
    292          password=stanza.xpath_eval("a:query/a:password",{"a":"jabber:iq:auth"})
    293          if password:
    294              password=from_utf8(password[0].getContent())
    295          if not password:
    296              self.__logger.debug("No password found in plain auth request")
    297              iq=stanza.make_error_response("bad-request")
    298              self.send(iq)
    299              return
    300
    301          if self.check_password(username,password):
    302              iq=stanza.make_result_response()
    303              self.send(iq)
    304              self.peer_authenticated=True
    305              self.auth_method_used="plain"
    306              self.state_change("authorized",self.peer)
    307              self._post_auth()
    308          else:
    309              self.__logger.debug("Plain auth failed")
    310              iq=stanza.make_error_response("bad-request")
    311              e=iq.get_error()
    312              e.add_custom_condition('jabber:iq:auth:error',"user-unauthorized")
    313              self.send(iq)
    314
    315 -    def _digest_auth_stage2(self, _unused):
    316          """Do the second stage (<iq type='set'/>) of legacy "digest"
    317          authentication.
    318
    319          [client only]"""
    320          iq=Iq(stanza_type="set")
    321          q=iq.new_query("jabber:iq:auth")
    322          q.newTextChild(None,"username",to_utf8(self.my_jid.node))
    323          q.newTextChild(None,"resource",to_utf8(self.my_jid.resource))
    324
    325          digest = sha_factory(to_utf8(self.stream_id)+to_utf8(self.password)).hexdigest()
    326
    327          q.newTextChild(None,"digest",digest)
    328          self.send(iq)
    329          self.set_response_handlers(iq,self.auth_finish,self.auth_error)
    330          iq.free()
    331
    332 -    def _digest_auth_in_stage2(self, username, _unused, stanza):
    333          """Handle the second stage (<iq type='set'/>) of legacy "digest"
    334          authentication.
    335
    336          [server only]"""
    337          digest=stanza.xpath_eval("a:query/a:digest",{"a":"jabber:iq:auth"})
    338          if digest:
    339              digest=digest[0].getContent()
    340          if not digest:
    341              self.__logger.debug("No digest found in digest auth request")
    342              iq=stanza.make_error_response("bad-request")
    343              self.send(iq)
    344              return
    345
    346          password,pwformat=self.get_password(username)
    347          if not password or pwformat!="plain":
    348              iq=stanza.make_error_response("bad-request")
    349              e=iq.get_error()
    350              e.add_custom_condition('jabber:iq:auth:error',"user-unauthorized")
    351              self.send(iq)
    352              return
    353
    354          mydigest = sha_factory(to_utf8(self.stream_id)+to_utf8(password)).hexdigest()
    355
    356          if mydigest==digest:
    357              iq=stanza.make_result_response()
    358              self.send(iq)
    359              self.peer_authenticated=True
    360              self.auth_method_used="digest"
    361              self.state_change("authorized",self.peer)
    362              self._post_auth()
    363          else:
    364              self.__logger.debug("Digest auth failed: %r != %r" % (digest,mydigest))
    365              iq=stanza.make_error_response("bad-request")
    366              e=iq.get_error()
    367              e.add_custom_condition('jabber:iq:auth:error',"user-unauthorized")
    368              self.send(iq)
    369
    370 -    def auth_finish(self, _unused):
    371          """Handle success of the legacy authentication."""
    372          self.lock.acquire()
    373          try:
    374              self.__logger.debug("Authenticated")
    375              self.authenticated=True
    376              self.state_change("authorized",self.my_jid)
    377              self._post_auth()
    378          finally:
    379              self.lock.release()
    380
    381 -    def registration_error(self, stanza):
    382          """Handle in-band registration error.
    383
    384          [client only]
    385
    386          :Parameters:
    387              - `stanza`: the error stanza received or `None` on timeout.
    388          :Types:
    389              - `stanza`: `pyxmpp.stanza.Stanza`"""
    390          self.lock.acquire()
    391          try:
    392              err=stanza.get_error()
    393              ae=err.xpath_eval("e:*",{"e":"jabber:iq:auth:error"})
    394              if ae:
    395                  ae=ae[0].name
    396              else:
    397                  ae=err.get_condition().name
    398              raise RegistrationError,("Authentication error condition: %s" % (ae,))
    399          finally:
    400              self.lock.release()
    401
    402 -    def registration_form_received(self, stanza):
    403          """Handle registration form received.
    404
    405          [client only]
    406
    407          Call self.registration_callback with the registration form received
    408          as the argument. Use the value returned by the callback will be a
    409          filled-in form.
    410
    411          :Parameters:
    412              - `stanza`: the stanza received.
    413          :Types:
    414              - `stanza`: `pyxmpp.iq.Iq`"""
    415          self.lock.acquire()
    416          try:
    417              self.__register = Register(stanza.get_query())
    418              self.registration_callback(stanza, self.__register.get_form())
    419          finally:
    420              self.lock.release()
    421
    422 -    def submit_registration_form(self, form):
    423          """Submit a registration form.
    424
    425          [client only]
    426
    427          :Parameters:
    428              - `form`: the filled-in form. When form is `None` or its type is
    429                "cancel" the registration is to be canceled.
    430
    431          :Types:
    432              - `form`: `pyxmpp.jabber.dataforms.Form`"""
    433          self.lock.acquire()
    434          try:
    435              if form and form.type!="cancel":
    436                  self.registration_form = form
    437                  iq = Iq(stanza_type = "set")
    438                  iq.set_content(self.__register.submit_form(form))
    439                  self.set_response_handlers(iq, self.registration_success, self.registration_error)
    440                  self.send(iq)
    441              else:
    442                  self.__register = None
    443          finally:
    444              self.lock.release()
    445
    446 -    def registration_success(self, stanza):
    447          """Handle registration success.
    448
    449          [client only]
    450
    451          Clean up registration stuff, change state to "registered" and initialize
    452          authentication.
    453
    454          :Parameters:
    455              - `stanza`: the stanza received.
    456          :Types:
    457              - `stanza`: `pyxmpp.iq.Iq`"""
    458          _unused = stanza
    459          self.lock.acquire()
    460          try:
    461              self.state_change("registered", self.registration_form)
    462              if ('FORM_TYPE' in self.registration_form
    463                      and self.registration_form['FORM_TYPE'].value == 'jabber:iq:register'):
    464                  if 'username' in self.registration_form:
    465                      self.my_jid = JID(self.registration_form['username'].value,
    466                              self.my_jid.domain, self.my_jid.resource)
    467                  if 'password' in self.registration_form:
    468                      self.password = self.registration_form['password'].value
    469              self.registration_callback = None
    470              self._post_connect()
    471          finally:
    472              self.lock.release()
    473
    474  # vi: sts=4 et sw=4
    475