Package pyxmpp :: Package jabber :: Module clientstream

[hide private]

# Source Code for Module pyxmpp.jabber.clientstream

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