Package pyxmpp :: Module streamsasl |
[hide private]

# Source Code for Module pyxmpp.streamsasl

      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  # pylint: disable-msg=W0201
     18
     19  """SASL support XMPP streams.
     20
     21  Normative reference:
     22    - `RFC 3920 <http://www.ietf.org/rfc/rfc3920.txt>`__
     23  """
     24
     25  __docformat__="restructuredtext en"
     26
     27  import base64
     28  import logging
     29
     30  from pyxmpp.jid import JID
     31  from pyxmpp import sasl
     32  from pyxmpp.exceptions import StreamAuthenticationError, SASLNotAvailable, SASLMechanismNotAvailable, SASLAuthenticationFailed
     33
     34  SASL_NS="urn:ietf:params:xml:ns:xmpp-sasl"
     35
     36 -class StreamSASLMixIn(sasl.PasswordManager):
     37      """SASL authentication mix-in class for XMPP stream."""
     38 -    def __init__(self,sasl_mechanisms=()):
     39          """Initialize Stream object
     40
     41          :Parameters:
     42            - `sasl_mechanisms`: sequence of SASL mechanisms allowed for
     43              authentication. Currently "PLAIN", "DIGEST-MD5" and "GSSAPI" are supported.
     44          """
     45          sasl.PasswordManager.__init__(self)
     46          if sasl_mechanisms:
     47              self.sasl_mechanisms=sasl_mechanisms
     48          else:
     49              self.sasl_mechanisms=[]
     50          self.__logger=logging.getLogger("pyxmpp.StreamSASLMixIn")
     51
     52 -    def _reset_sasl(self):
     53          """Reset `StreamSASLMixIn` object state making it ready to handle new
     54          connections."""
     55          self.peer_sasl_mechanisms=None
     56          self.authenticator=None
     57
     58 -    def _make_stream_sasl_features(self,features):
     59          """Add SASL features to the <features/> element of the stream.
     60
     61          [receving entity only]
     62
     63          :returns: update <features/> element node."""
     64          if self.sasl_mechanisms and not self.authenticated:
     65              ml=features.newChild(None,"mechanisms",None)
     66              ns=ml.newNs(SASL_NS,None)
     67              ml.setNs(ns)
     68              for m in self.sasl_mechanisms:
     69                  if m in sasl.all_mechanisms:
     70                      ml.newTextChild(None,"mechanism",m)
     71          return features
     72
     73 -    def _handle_sasl_features(self):
     74          """Process incoming <stream:features/> element.
     75
     76          [initiating entity only]
     77
     78          The received features node is available in `self.features`."""
     79          ctxt = self.doc_in.xpathNewContext()
     80          ctxt.setContextNode(self.features)
     81          ctxt.xpathRegisterNs("sasl",SASL_NS)
     82          try:
     83              sasl_mechanisms_n=ctxt.xpathEval("sasl:mechanisms/sasl:mechanism")
     84          finally:
     85              ctxt.xpathFreeContext()
     86
     87          if sasl_mechanisms_n:
     88              self.__logger.debug("SASL support found")
     89              self.peer_sasl_mechanisms=[]
     90              for n in sasl_mechanisms_n:
     91                  self.peer_sasl_mechanisms.append(n.getContent())
     92
     93 -    def _process_node_sasl(self,xmlnode):
     94          """Process incoming stream element. Pass it to _process_sasl_node
     95          if it is in the SASL namespace.
     96
     97          :return: `True` when the node was recognized as a SASL element.
     98          :returntype: `bool`"""
     99          ns_uri=xmlnode.ns().getContent()
    100          if ns_uri==SASL_NS:
    101              self._process_sasl_node(xmlnode)
    102              return True
    103          return False
    104
    105 -    def _process_sasl_node(self,xmlnode):
    106          """Process stream element in the SASL namespace.
    107
    108          :Parameters:
    109              - `xmlnode`: the XML node received
    110          """
    111          if self.initiator:
    112              if not self.authenticator:
    113                  self.__logger.debug("Unexpected SASL response: %r" % (xmlnode.serialize()))
    114                  ret=False
    115              elif xmlnode.name=="challenge":
    116                  ret=self._process_sasl_challenge(xmlnode.getContent())
    117              elif xmlnode.name=="success":
    118                  ret=self._process_sasl_success(xmlnode.getContent())
    119              elif xmlnode.name=="failure":
    120                  ret=self._process_sasl_failure(xmlnode)
    121              else:
    122                  self.__logger.debug("Unexpected SASL node: %r" % (xmlnode.serialize()))
    123                  ret=False
    124          else:
    125              if xmlnode.name=="auth":
    126                  mechanism=xmlnode.prop("mechanism")
    127                  ret=self._process_sasl_auth(mechanism,xmlnode.getContent())
    128              if xmlnode.name=="response":
    129                  ret=self._process_sasl_response(xmlnode.getContent())
    130              if xmlnode.name=="abort":
    131                  ret=self._process_sasl_abort()
    132              else:
    133                  self.__logger.debug("Unexpected SASL node: %r" % (xmlnode.serialize()))
    134                  ret=False
    135          return ret
    136
    137 -    def _process_sasl_auth(self,mechanism,content):
    138          """Process incoming <sasl:auth/> element.
    139
    140          [receiving entity only]
    141
    142          :Parameters:
    143              - `mechanism`: mechanism choosen by the peer.
    144              - `content`: optional "initial response" included in the element.
    145          """
    146          if self.authenticator:
    147              self.__logger.debug("Authentication already started")
    148              return False
    149
    150          self.auth_method_used="sasl:"+mechanism
    151          self.authenticator=sasl.server_authenticator_factory(mechanism,self)
    152
    153          r=self.authenticator.start(base64.decodestring(content))
    154
    155          if isinstance(r,sasl.Success):
    156              el_name="success"
    157              content=r.base64()
    158          elif isinstance(r,sasl.Challenge):
    159              el_name="challenge"
    160              content=r.base64()
    161          else:
    162              el_name="failure"
    163              content=None
    164
    165          root=self.doc_out.getRootElement()
    166          xmlnode=root.newChild(None,el_name,None)
    167          ns=xmlnode.newNs(SASL_NS,None)
    168          xmlnode.setNs(ns)
    169          if content:
    170              xmlnode.setContent(content)
    171          if isinstance(r,sasl.Failure):
    172              xmlnode.newChild(None,r.reason,None)
    173
    174          self._write_raw(xmlnode.serialize(encoding="UTF-8"))
    175          xmlnode.unlinkNode()
    176          xmlnode.freeNode()
    177
    178          if isinstance(r,sasl.Success):
    179              if r.authzid:
    180                  self.peer=JID(r.authzid)
    181              else:
    182                  self.peer=JID(r.username,self.me.domain)
    183              self.peer_authenticated=1
    184              self.state_change("authenticated",self.peer)
    185              self._post_auth()
    186
    187          if isinstance(r,sasl.Failure):
    188              raise SASLAuthenticationFailed,"SASL authentication failed"
    189
    190          return True
    191
    192 -    def _process_sasl_challenge(self,content):
    193          """Process incoming <sasl:challenge/> element.
    194
    195          [initiating entity only]
    196
    197          :Parameters:
    198              - `content`: the challenge data received (Base64-encoded).
    199          """
    200          if not self.authenticator:
    201              self.__logger.debug("Unexpected SASL challenge")
    202              return False
    203
    204          r=self.authenticator.challenge(base64.decodestring(content))
    205          if isinstance(r,sasl.Response):
    206              el_name="response"
    207              content=r.base64()
    208          else:
    209              el_name="abort"
    210              content=None
    211
    212          root=self.doc_out.getRootElement()
    213          xmlnode=root.newChild(None,el_name,None)
    214          ns=xmlnode.newNs(SASL_NS,None)
    215          xmlnode.setNs(ns)
    216          if content:
    217              xmlnode.setContent(content)
    218
    219          self._write_raw(xmlnode.serialize(encoding="UTF-8"))
    220          xmlnode.unlinkNode()
    221          xmlnode.freeNode()
    222
    223          if isinstance(r,sasl.Failure):
    224              raise SASLAuthenticationFailed,"SASL authentication failed"
    225
    226          return True
    227
    228 -    def _process_sasl_response(self,content):
    229          """Process incoming <sasl:response/> element.
    230
    231          [receiving entity only]
    232
    233          :Parameters:
    234              - `content`: the response data received (Base64-encoded).
    235          """
    236          if not self.authenticator:
    237              self.__logger.debug("Unexpected SASL response")
    238              return 0
    239
    240          r=self.authenticator.response(base64.decodestring(content))
    241          if isinstance(r,sasl.Success):
    242              el_name="success"
    243              content=r.base64()
    244          elif isinstance(r,sasl.Challenge):
    245              el_name="challenge"
    246              content=r.base64()
    247          else:
    248              el_name="failure"
    249              content=None
    250
    251          root=self.doc_out.getRootElement()
    252          xmlnode=root.newChild(None,el_name,None)
    253          ns=xmlnode.newNs(SASL_NS,None)
    254          xmlnode.setNs(ns)
    255          if content:
    256              xmlnode.setContent(content)
    257          if isinstance(r,sasl.Failure):
    258              xmlnode.newChild(None,r.reason,None)
    259
    260          self._write_raw(xmlnode.serialize(encoding="UTF-8"))
    261          xmlnode.unlinkNode()
    262          xmlnode.freeNode()
    263
    264          if isinstance(r,sasl.Success):
    265              authzid=r.authzid
    266              if authzid:
    267                  self.peer=JID(r.authzid)
    268              else:
    269                  self.peer=JID(r.username,self.me.domain)
    270              self.peer_authenticated=1
    271              self._restart_stream()
    272              self.state_change("authenticated",self.peer)
    273              self._post_auth()
    274
    275          if isinstance(r,sasl.Failure):
    276              raise SASLAuthenticationFailed,"SASL authentication failed"
    277
    278          return 1
    279
    280 -    def _process_sasl_success(self,content):
    281          """Process incoming <sasl:success/> element.
    282
    283          [initiating entity only]
    284
    285          :Parameters:
    286              - `content`: the "additional data with success" received (Base64-encoded).
    287          """
    288          if not self.authenticator:
    289              self.__logger.debug("Unexpected SASL response")
    290              return False
    291
    292          r=self.authenticator.finish(base64.decodestring(content))
    293          if isinstance(r,sasl.Success):
    294              self.__logger.debug("SASL authentication succeeded")
    295              if r.authzid:
    296                  self.me=JID(r.authzid)
    297              else:
    298                  self.me=self.me
    299              self.authenticated=1
    300              self._restart_stream()
    301              self.state_change("authenticated",self.me)
    302              self._post_auth()
    303          else:
    304              self.__logger.debug("SASL authentication failed")
    305              raise SASLAuthenticationFailed,"Additional success data procesing failed"
    306          return True
    307
    308 -    def _process_sasl_failure(self,xmlnode):
    309          """Process incoming <sasl:failure/> element.
    310
    311          [initiating entity only]
    312
    313          :Parameters:
    314              - `xmlnode`: the XML node received.
    315          """
    316          if not self.authenticator:
    317              self.__logger.debug("Unexpected SASL response")
    318              return False
    319
    320          self.__logger.debug("SASL authentication failed: %r" % (xmlnode.serialize(),))
    321          raise SASLAuthenticationFailed,"SASL authentication failed"
    322
    323 -    def _process_sasl_abort(self):
    324          """Process incoming <sasl:abort/> element.
    325
    326          [receiving entity only]"""
    327          if not self.authenticator:
    328              self.__logger.debug("Unexpected SASL response")
    329              return False
    330
    331          self.authenticator=None
    332          self.__logger.debug("SASL authentication aborted")
    333          return True
    334
    335 -    def _sasl_authenticate(self,username,authzid,mechanism=None):
    336          """Start SASL authentication process.
    337
    338          [initiating entity only]
    339
    340          :Parameters:
    341              - `username`: user name.
    342              - `authzid`: authorization ID.
    343              - `mechanism`: SASL mechanism to use."""
    344          if not self.initiator:
    345              raise SASLAuthenticationFailed,"Only initiating entity start SASL authentication"
    346          while not self.features:
    347              self.__logger.debug("Waiting for features")
    348              self._read()
    349          if not self.peer_sasl_mechanisms:
    350              raise SASLNotAvailable,"Peer doesn't support SASL"
    351
    352          if not mechanism:
    353              mechanism=None
    354              for m in self.sasl_mechanisms:
    355                  if m in self.peer_sasl_mechanisms:
    356                      mechanism=m
    357                      break
    358              if not mechanism:
    359                  raise SASLMechanismNotAvailable,"Peer doesn't support any of our SASL mechanisms"
    360              self.__logger.debug("Our mechanism: %r" % (mechanism,))
    361          else:
    362              if mechanism not in self.peer_sasl_mechanisms:
    363                  raise SASLMechanismNotAvailable,"%s is not available" % (mechanism,)
    364
    365          self.auth_method_used="sasl:"+mechanism
    366
    367          self.authenticator=sasl.client_authenticator_factory(mechanism,self)
    368
    369          initial_response=self.authenticator.start(username,authzid)
    370          if not isinstance(initial_response,sasl.Response):
    371              raise SASLAuthenticationFailed,"SASL initiation failed"
    372
    373          root=self.doc_out.getRootElement()
    374          xmlnode=root.newChild(None,"auth",None)
    375          ns=xmlnode.newNs(SASL_NS,None)
    376          xmlnode.setNs(ns)
    377          xmlnode.setProp("mechanism",mechanism)
    378          if initial_response.data:
    379              if initial_response.encode:
    380                  xmlnode.setContent(initial_response.base64())
    381              else:
    382                  xmlnode.setContent(initial_response.data)
    383
    384          self._write_raw(xmlnode.serialize(encoding="UTF-8"))
    385          xmlnode.unlinkNode()
    386          xmlnode.freeNode()
    387
    388  # vi: sts=4 et sw=4
    389