# -*- coding: utf-8 -*- # xmppony # auth.py # Copyright (c) 2009 Anaƫl Verrier # Copyright (c) 2003-2009 Alexey "Snake" Nezhdanov # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3 only. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA """ Provides library with all Non-SASL and SASL authentication mechanisms. Can be used both for client and transport authentication. """ import base64 import random import re try: from hashlib import md5, sha1 except ImportError: from md5 import new as md5 from sha import new as sha1 from client import PlugIn import dispatcher from protocol import (Iq, isResultNode, JID, Node, NodeProcessed, NS_AUTH, NS_BIND, NS_COMPONENT_1, NS_COMPONENT_ACCEPT, NS_SASL, NS_SESSION, NS_STREAMS, Protocol) def SH(some): return sha1(some).hexdigest() def HH(some): return md5(some).hexdigest() def H(some): return md5(some).digest() def C(some): return ':'.join(some) class NonSASL(PlugIn): """ Implements old Non-SASL (JEP-0078) authentication used in jabberd1.4 and transport authentication.""" def __init__(self, user, password, resource): """ Caches username, password and resource for auth. """ PlugIn.__init__(self) self.DBG_LINE = 'gen_auth' self.user = user self.password = password self.resource = resource def plugin(self, owner): """ Determine the best auth method (digest/0k/plain) and use it for auth. Returns used method name on success. Used internally. """ if not self.resource: return self.authComponent(owner) self.DEBUG('Querying server about possible auth methods', 'start') resp = owner.Dispatcher.SendAndWaitForResponse( Iq('get', NS_AUTH, payload=[Node('username', payload=[self.user])])) if not isResultNode(resp): self.DEBUG('No result node arrived! Aborting...', 'error') return iq = Iq(typ='set', node=resp) query = iq.getTag('query') query.setTagData('username', self.user) query.setTagData('resource', self.resource) if query.getTag('digest'): self.DEBUG("Performing digest authentication", 'ok') query.setTagData( 'digest', SH(owner.Dispatcher.Stream._document_attrs['id'] + self.password)) if query.getTag('password'): query.delChild('password') method = 'digest' elif query.getTag('token'): token = query.getTagData('token') seq = query.getTagData('sequence') self.DEBUG("Performing zero-k authentication", 'ok') hash_ = SH(SH(self.password) + token) for _ in xrange(int(seq)): hash_ = SH(hash_) query.setTagData('hash', hash_) method = '0k' else: self.DEBUG("Sequre methods unsupported, performing plain text authentication", 'warn') query.setTagData('password', self.password) method = 'plain' resp = owner.Dispatcher.SendAndWaitForResponse(iq) if isResultNode(resp): self.DEBUG('Sucessfully authenticated with remove host.', 'ok') owner.User = self.user owner.Resource = self.resource owner._registered_name = ('%(user)s@%(server)s/%(resource)s' % {'user': owner.User, 'server': owner.Server, 'resource': owner.Resource}) return method self.DEBUG('Authentication failed!', 'error') def authComponent(self, owner): """ Authenticate component. Send handshake stanza and wait for result. Returns "ok" on success. """ self.handshake = 0 owner.send( Node(NS_COMPONENT_ACCEPT + ' handshake', payload=[SH(owner.Dispatcher.Stream._document_attrs['id'] + self.password)])) owner.RegisterHandler('handshake', self.handshakeHandler, xmlns=NS_COMPONENT_ACCEPT) while not self.handshake: self.DEBUG("waiting on handshake", 'notify') owner.Process(1) owner._registered_name = self.user if self.handshake + 1: return 'ok' def handshakeHandler(self, disp, stanza): """ Handler for registering in dispatcher for accepting transport authentication. """ if stanza.getName() == 'handshake': self.handshake = 1 else: self.handshake = -1 class SASL(PlugIn): """ Implements SASL authentication. """ def __init__(self, username, password): PlugIn.__init__(self) self.username = username self.password = password def plugin(self, owner): if 'version' not in self._owner.Dispatcher.Stream._document_attrs: self.startsasl = 'not-supported' elif self._owner.Dispatcher.Stream.features: try: self.FeaturesHandler(self._owner.Dispatcher, self._owner.Dispatcher.Stream.features) except NodeProcessed: pass else: self.startsasl = None def auth(self): """ Start authentication. Result can be obtained via "SASL.startsasl" attribute and will be either "success" or "failure". Note that successfull auth will take at least two Dispatcher.Process() calls. """ if self.startsasl: pass elif self._owner.Dispatcher.Stream.features: try: self.FeaturesHandler(self._owner.Dispatcher, self._owner.Dispatcher.Stream.features) except NodeProcessed: pass else: self._owner.RegisterHandler('features', self.FeaturesHandler, xmlns=NS_STREAMS) def plugout(self): """ Remove SASL handlers from owner's dispatcher. Used internally. """ if 'features' in self._owner.__dict__: self._owner.UnregisterHandler('features', self.FeaturesHandler, xmlns=NS_STREAMS) if 'challenge' in self._owner.__dict__: self._owner.UnregisterHandler('challenge', self.SASLHandler, xmlns=NS_SASL) if 'failure' in self._owner.__dict__: self._owner.UnregisterHandler('failure', self.SASLHandler, xmlns=NS_SASL) if 'success' in self._owner.__dict__: self._owner.UnregisterHandler('success', self.SASLHandler, xmlns=NS_SASL) def FeaturesHandler(self, conn, feats): """ Used to determine if server supports SASL auth. Used internally. """ if not feats.getTag('mechanisms', namespace=NS_SASL): self.startsasl = 'not-supported' self.DEBUG('SASL not supported by server', 'error') return mecs = list() for mec in feats.getTag('mechanisms', namespace=NS_SASL).getTags('mechanism'): mecs.append(mec.getData()) self._owner.RegisterHandler('challenge', self.SASLHandler, xmlns=NS_SASL) self._owner.RegisterHandler('failure', self.SASLHandler, xmlns=NS_SASL) self._owner.RegisterHandler('success', self.SASLHandler, xmlns=NS_SASL) if "ANONYMOUS" in mecs and self.username is None: node = Node('auth', attrs={'xmlns': NS_SASL, 'mechanism': 'ANONYMOUS'}) elif "DIGEST-MD5" in mecs: node = Node('auth', attrs={'xmlns': NS_SASL, 'mechanism': 'DIGEST-MD5'}) elif "PLAIN" in mecs: sasl_data = '%s\x00%s\x00%s' % ( self.username + '@' + self._owner.Server, self.username, self.password) node = Node( 'auth', attrs={'xmlns': NS_SASL, 'mechanism': 'PLAIN'}, payload=[ base64.encodestring(sasl_data).replace('\r', '').replace('\n', '')]) else: self.startsasl = 'failure' self.DEBUG('I can only use DIGEST-MD5 and PLAIN mecanisms.', 'error') return self.startsasl = 'in-process' self._owner.send(node.__str__()) raise NodeProcessed def SASLHandler(self, conn, challenge): """ Perform next SASL auth step. Used internally. """ if challenge.getNamespace() != NS_SASL: return if challenge.getName() == 'failure': self.startsasl = 'failure' try: reason = challenge.getChildren()[0] except: reason = challenge self.DEBUG('Failed SASL authentification: %s' % reason, 'error') raise NodeProcessed elif challenge.getName() == 'success': self.startsasl = 'success' self.DEBUG('Successfully authenticated with remote server.', 'ok') handlers = self._owner.Dispatcher.dumpHandlers() self._owner.Dispatcher.PlugOut() dispatcher.Dispatcher().PlugIn(self._owner) self._owner.Dispatcher.restoreHandlers(handlers) self._owner.User = self.username raise NodeProcessed incoming_data = challenge.getData() chal = dict() data = base64.decodestring(incoming_data) self.DEBUG('Got challenge:' + data, 'ok') for pair in re.findall('(\w+\s*=\s*(?:(?:"[^"]+")|(?:[^,]+)))', data): key, value = [x.strip() for x in pair.split('=', 1)] if value[:1] == '"' and value[-1:] == '"': value = value[1:-1] chal[key] = value if 'qop' in chal and 'auth' in [x.strip() for x in chal['qop'].split(',')]: resp = dict() resp['username'] = self.username resp['realm'] = self._owner.Server resp['nonce'] = chal['nonce'] cnonce = '' for _ in xrange(7): cnonce += hex(int(random.random() * 65536 * 4096))[2:] resp['cnonce'] = cnonce resp['nc'] = ('00000001') resp['qop'] = 'auth' resp['digest-uri'] = 'xmpp/' + self._owner.Server A1 = C([H(C([resp['username'], resp['realm'], self.password])), resp['nonce'], resp['cnonce']]) A2 = C(['AUTHENTICATE', resp['digest-uri']]) response = HH(C([HH(A1), resp['nonce'], resp['nc'], resp['cnonce'], resp['qop'], HH(A2)])) resp['response'] = response resp['charset'] = 'utf-8' sasl_data = '' for key in ('username', 'realm', 'nonce', 'cnonce', 'digest-uri'): sasl_data += '%s="%s",' % (key, resp[key]) for key in ('nc', 'qop', 'response', 'charset'): sasl_data += "%s=%s," % (key, resp[key]) node = Node( 'response', attrs={'xmlns': NS_SASL}, payload=[base64.encodestring(sasl_data[:-1]).replace( '\r', '').replace('\n', '')]) self._owner.send(node.__str__()) elif 'rspauth' in chal: self._owner.send(Node('response', attrs={'xmlns': NS_SASL}).__str__()) else: self.startsasl = 'failure' self.DEBUG('Failed SASL authentification: unknown challenge', 'error') raise NodeProcessed class Bind(PlugIn): """ Bind some JID to the current connection to allow router know of our location.""" def __init__(self): PlugIn.__init__(self) self.DBG_LINE = 'bind' self.bound = None def plugin(self, owner): """ Start resource binding, if allowed at this time. Used internally. """ if self._owner.Dispatcher.Stream.features: try: self.FeaturesHandler(self._owner.Dispatcher, self._owner.Dispatcher.Stream.features) except NodeProcessed: pass else: self._owner.RegisterHandler('features', self.FeaturesHandler, xmlns=NS_STREAMS) def plugout(self): """ Remove Bind handler from owner's dispatcher. Used internally. """ self._owner.UnregisterHandler('features', self.FeaturesHandler, xmlns=NS_STREAMS) def FeaturesHandler(self, conn, feats): """ Determine if server supports resource binding and set some internal attributes accordingly. """ if not feats.getTag('bind', namespace=NS_BIND): self.bound = 'failure' self.DEBUG('Server does not requested binding.', 'error') return if feats.getTag('session', namespace=NS_SESSION): self.session = 1 else: self.session = -1 self.bound = list() def Bind(self, resource=None): """ Perform binding. Use provided resource name or random (if not provided). """ while self.bound is None and self._owner.Process(1): pass if resource: resource = [Node('resource', payload=[resource])] else: resource = list() resp = self._owner.SendAndWaitForResponse( Protocol('iq', typ='set', payload=[Node('bind', attrs={'xmlns': NS_BIND}, payload=resource)])) if isResultNode(resp): self.bound.append(resp.getTag('bind').getTagData('jid')) self.DEBUG('Successfully bound %s.' % self.bound[-1], 'ok') jid = JID(resp.getTag('bind').getTagData('jid')) self._owner.User = jid.getNode() self._owner.Resource = jid.getResource() resp = self._owner.SendAndWaitForResponse( Protocol('iq', typ='set', payload=[Node('session', attrs={'xmlns': NS_SESSION})])) if isResultNode(resp): self.DEBUG('Successfully opened session.', 'ok') self.session = 1 return 'ok' else: self.DEBUG('Session open failed.', 'error') self.session = 0 elif resp: self.DEBUG('Binding failed: %s.' % resp.getTag('error'), 'error') else: self.DEBUG('Binding failed: timeout expired.', 'error') return '' class ComponentBind(PlugIn): """ ComponentBind some JID to the current connection to allow router know of our location.""" def __init__(self, sasl): PlugIn.__init__(self) self.DBG_LINE = 'bind' self.bound = None self.needsUnregister = None self.sasl = sasl def plugin(self, owner): """ Start resource binding, if allowed at this time. Used internally. """ if not self.sasl: self.bound = list() return if self._owner.Dispatcher.Stream.features: try: self.FeaturesHandler(self._owner.Dispatcher, self._owner.Dispatcher.Stream.features) except NodeProcessed: pass else: self._owner.RegisterHandler('features', self.FeaturesHandler, xmlns=NS_STREAMS) self.needsUnregister = 1 def plugout(self): """ Remove ComponentBind handler from owner's dispatcher. Used internally. """ if self.needsUnregister: self._owner.UnregisterHandler('features', self.FeaturesHandler, xmlns=NS_STREAMS) def FeaturesHandler(self, conn, feats): """ Determine if server supports resource binding and set some internal attributes accordingly. """ if not feats.getTag('bind', namespace=NS_BIND): self.bound = 'failure' self.DEBUG('Server does not requested binding.', 'error') return if feats.getTag('session', namespace=NS_SESSION): self.session = 1 else: self.session = -1 self.bound = list() def Bind(self, domain=None): """ Perform binding. Use provided domain name (if not provided). """ while self.bound is None and self._owner.Process(1): pass if self.sasl: xmlns = NS_COMPONENT_1 else: xmlns = None self.bindresponse = None ttl = dispatcher.DefaultTimeout self._owner.RegisterHandler('bind', self.BindHandler, xmlns=xmlns) self._owner.send(Protocol('bind', attrs={'name': domain}, xmlns=NS_COMPONENT_1)) while self.bindresponse is None and self._owner.Process(1) and ttl > 0: ttl -= 1 self._owner.UnregisterHandler('bind', self.BindHandler, xmlns=xmlns) resp = self.bindresponse if resp and resp.getAttr('error'): self.DEBUG('Binding failed: %s.' % resp.getAttr('error'), 'error') elif resp: self.DEBUG('Successfully bound.', 'ok') return 'ok' else: self.DEBUG('Binding failed: timeout expired.', 'error') return '' def BindHandler(self, conn, bind): self.bindresponse = bind