pyxmpp.jabber.clientstream

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