pyxmpp.streamsasl

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