##   auth_nb.py
##       based on auth.py, changes backported up to revision 1.41
##
##   Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
##       modified by Dimitur Kirov <dkirov@gmail.com>
##
##   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; either version 2, or (at your option)
##   any later version.
##
##   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.
'''
Provides plugs for SASL and NON-SASL authentication mechanisms.
Can be used both for client and transport authentication.

See client_nb.py
'''
from protocol import NS_SASL, NS_SESSION, NS_STREAMS, NS_BIND, NS_AUTH
from protocol import Node, NodeProcessed, isResultNode, Iq, Protocol, JID
from plugin import PlugIn
import base64
import random
import itertools
import dispatcher_nb
import hashlib

import logging
log = logging.getLogger('gajim.c.x.auth_nb')

def HH(some): return hashlib.md5(some).hexdigest()
def H(some): return hashlib.md5(some).digest()
def C(some): return ':'.join(some)

try:
	import kerberos
	have_kerberos = True
except ImportError:
	have_kerberos = False

GSS_STATE_STEP = 0
GSS_STATE_WRAP = 1
SASL_FAILURE = 'failure'
SASL_SUCCESS = 'success'
SASL_UNSUPPORTED = 'not-supported'
SASL_IN_PROCESS = 'in-process'

def challenge_splitter(data):
	''' Helper function that creates a dict from challenge string.

	Sample challenge string:
		username="example.org",realm="somerealm",\
		nonce="OA6MG9tEQGm2hh",cnonce="OA6MHXh6VqTrRk",\
		nc=00000001,qop="auth,auth-int,auth-conf",charset=utf-8

	Expected result for challan:
		dict['qop'] = ('auth','auth-int','auth-conf')
		dict['realm'] = 'somerealm'
	'''
	X_KEYWORD, X_VALUE, X_END = 0, 1, 2
	quotes_open = False
	keyword, value = '', ''
	dict_ = {}
	arr = None

	expecting = X_KEYWORD
	for iter_ in range(len(data) + 1):
		end = False
		if iter_ == len(data):
			expecting = X_END
			end = True
		else:
			char = data[iter_]
		if expecting == X_KEYWORD:
			if char == '=':
				expecting  = X_VALUE
			elif char in (',', ' ', '\t'):
				pass
			else:
				keyword = '%s%c' % (keyword, char)
		elif expecting == X_VALUE:
			if char == '"':
				if quotes_open:
					end = True
				else:
					quotes_open = True
			elif char in (',', ' ', '\t'):
				if quotes_open:
					if not arr:
						arr = [value]
					else:
						arr.append(value)
					value = ""
				else:
					end = True
			else:
				value = '%s%c' % (value, char)
		if end:
			if arr:
				arr.append(value)
				dict_[keyword] = arr
				arr = None
			else:
				dict_[keyword] = value
			value, keyword = '', ''
			expecting = X_KEYWORD
			quotes_open = False
	return dict_


class SASL(PlugIn):
	'''
	Implements SASL authentication. Can be plugged into NonBlockingClient
	to start authentication.
	'''
	def __init__(self, username, password, on_sasl):
		'''
		:param user: XMPP username
		:param password: XMPP password
		:param on_sasl: Callback, will be called after each SASL auth-step.
		'''
		PlugIn.__init__(self)
		self.username = username
		self.password = password
		self.on_sasl = on_sasl
		self.realm = None

	def plugin(self, owner):
		if 'version' not in self._owner.Dispatcher.Stream._document_attrs:
			self.startsasl = SASL_UNSUPPORTED
		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 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 auth(self):
		'''
		Start authentication. Result can be obtained via "SASL.startsasl"
		attribute and will be either SASL_SUCCESS or SASL_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 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'
			log.error('SASL not supported by server')
			return
		self.mecs = []
		for mec in feats.getTag('mechanisms', namespace=NS_SASL).getTags(
		'mechanism'):
			self.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)
		self.MechanismHandler()

	def MechanismHandler(self):
		if 'ANONYMOUS' in self.mecs and self.username is None:
			self.mecs.remove('ANONYMOUS')
			node = Node('auth',attrs={'xmlns': NS_SASL, 'mechanism': 'ANONYMOUS'})
			self.mechanism = 'ANONYMOUS'
			self.startsasl = SASL_IN_PROCESS
			self._owner.send(str(node))
			raise NodeProcessed
		if 'GSSAPI' in self.mecs and have_kerberos:
			self.mecs.remove('GSSAPI')
			try:
				self.gss_vc = kerberos.authGSSClientInit('xmpp@' + \
					self._owner.xmpp_hostname)[1]
				kerberos.authGSSClientStep(self.gss_vc, '')
				response = kerberos.authGSSClientResponse(self.gss_vc)
				node=Node('auth',attrs={'xmlns': NS_SASL, 'mechanism': 'GSSAPI'},
					payload=(response or ''))
				self.mechanism = 'GSSAPI'
				self.gss_step = GSS_STATE_STEP
				self.startsasl = SASL_IN_PROCESS
				self._owner.send(str(node))
				raise NodeProcessed
			except kerberos.GSSError, e:
				log.info('GSSAPI authentication failed: %s' % str(e))
		if 'DIGEST-MD5' in self.mecs:
			self.mecs.remove('DIGEST-MD5')
			node = Node('auth',attrs={'xmlns': NS_SASL, 'mechanism': 'DIGEST-MD5'})
			self.mechanism = 'DIGEST-MD5'
			self.startsasl = SASL_IN_PROCESS
			self._owner.send(str(node))
			raise NodeProcessed
		if 'PLAIN' in self.mecs:
			self.mecs.remove('PLAIN')
			self.mechanism = 'PLAIN'
			self._owner._caller.get_password(self.set_password)
			self.startsasl = SASL_IN_PROCESS
			raise NodeProcessed
		self.startsasl = SASL_FAILURE
		log.error('I can only use DIGEST-MD5, GSSAPI and PLAIN mecanisms.')
		if self.on_sasl:
			self.on_sasl()
		return

	def SASLHandler(self, conn, challenge):
		''' Perform next SASL auth step. Used internally. '''
		if challenge.getNamespace() != NS_SASL:
			return
		### Handle Auth result
		if challenge.getName() == 'failure':
			self.startsasl = SASL_FAILURE
			try:
				reason = challenge.getChildren()[0]
			except Exception:
				reason = challenge
			log.error('Failed SASL authentification: %s' % reason)
			if len(self.mecs) > 0:
				# There are other mechanisms to test
				self.MechanismHandler()
				raise NodeProcessed
			if self.on_sasl:
				self.on_sasl()
			raise NodeProcessed
		elif challenge.getName() == 'success':
			self.startsasl = SASL_SUCCESS
			log.info('Successfully authenticated with remote server.')
			handlers = self._owner.Dispatcher.dumpHandlers()

			# Bosh specific dispatcher replugging
			# save old features. They will be used in case we won't get response on
			# stream restart after SASL auth (happens with XMPP over BOSH with
			# Openfire)
			old_features = self._owner.Dispatcher.Stream.features
			self._owner.Dispatcher.PlugOut()
			dispatcher_nb.Dispatcher.get_instance().PlugIn(self._owner,
				after_SASL=True, old_features=old_features)
			self._owner.Dispatcher.restoreHandlers(handlers)
			self._owner.User = self.username

			if self.on_sasl:
				self.on_sasl()
			raise NodeProcessed

		### Perform auth step
		incoming_data = challenge.getData()
		data=base64.decodestring(incoming_data)
		log.info('Got challenge:' + data)

		if self.mechanism == 'GSSAPI':
			if self.gss_step == GSS_STATE_STEP:
				rc = kerberos.authGSSClientStep(self.gss_vc, incoming_data)
				if rc != kerberos.AUTH_GSS_CONTINUE:
					self.gss_step = GSS_STATE_WRAP
			elif self.gss_step == GSS_STATE_WRAP:
				rc = kerberos.authGSSClientUnwrap(self.gss_vc, incoming_data)
				response = kerberos.authGSSClientResponse(self.gss_vc)
				rc = kerberos.authGSSClientWrap(self.gss_vc, response,
					kerberos.authGSSClientUserName(self.gss_vc))
			response = kerberos.authGSSClientResponse(self.gss_vc)
			if not response:
				response = ''
			self._owner.send(Node('response', attrs={'xmlns':NS_SASL},
				payload=response).__str__())
			raise NodeProcessed

		# magic foo...
		chal = challenge_splitter(data)
		if not self.realm and 'realm' in chal:
			self.realm = chal['realm']
		if 'qop' in chal and ((isinstance(chal['qop'], str) and \
		chal['qop'] =='auth') or (isinstance(chal['qop'], list) and 'auth' in \
		chal['qop'])):
			self.resp = {}
			self.resp['username'] = self.username
			if self.realm:
				self.resp['realm'] = self.realm
			else:
				self.resp['realm'] = self._owner.Server
			self.resp['nonce'] = chal['nonce']
			self.resp['cnonce'] = ''.join("%x" % randint(0, 2**28) for randint in
				itertools.repeat(random.randint, 7))
			self.resp['nc'] = ('00000001')
			self.resp['qop'] = 'auth'
			self.resp['digest-uri'] = 'xmpp/' + self._owner.Server
			self.resp['charset'] = 'utf-8'
			# Password is now required
			self._owner._caller.get_password(self.set_password)
		elif 'rspauth' in chal:
			self._owner.send(str(Node('response', attrs={'xmlns':NS_SASL})))
		else:
			self.startsasl = SASL_FAILURE
			log.error('Failed SASL authentification: unknown challenge')
		if self.on_sasl:
			self.on_sasl()
		raise NodeProcessed

	def set_password(self, password):
		if password is None:
			self.password = ''
		else:
			self.password = password
		if self.mechanism == 'DIGEST-MD5':
			def convert_to_iso88591(string):
				try:
					string = string.decode('utf-8').encode('iso-8859-1')
				except UnicodeEncodeError:
					pass
				return string
			hash_username = convert_to_iso88591(self.resp['username'])
			hash_realm = convert_to_iso88591(self.resp['realm'])
			hash_password = convert_to_iso88591(self.password)
			A1 = C([H(C([hash_username, hash_realm, hash_password])),
				self.resp['nonce'], self.resp['cnonce']])
			A2 = C(['AUTHENTICATE', self.resp['digest-uri']])
			response= HH(C([HH(A1), self.resp['nonce'], self.resp['nc'],
				self.resp['cnonce'], self.resp['qop'], HH(A2)]))
			self.resp['response'] = response
			sasl_data = u''
			for key in ('charset', 'username', 'realm', 'nonce', 'nc', 'cnonce',
			'digest-uri', 'response', 'qop'):
				if key in ('nc','qop','response','charset'):
					sasl_data += u"%s=%s," % (key, self.resp[key])
				else:
					sasl_data += u'%s="%s",' % (key, self.resp[key])
			sasl_data = sasl_data[:-1].encode('utf-8').encode('base64').replace(
				'\r', '').replace('\n', '')
			node = Node('response', attrs={'xmlns':NS_SASL}, payload=[sasl_data])
		elif self.mechanism == 'PLAIN':
			sasl_data = u'%s\x00%s\x00%s' % (self.username + '@' + \
				self._owner.Server, self.username, self.password)
			sasl_data = sasl_data.encode('utf-8').encode('base64').replace(
				'\n', '')
			node = Node('auth', attrs={'xmlns': NS_SASL, 'mechanism': 'PLAIN'},
				payload=[sasl_data])
		self._owner.send(str(node))


class NonBlockingNonSASL(PlugIn):
	'''
	Implements old Non-SASL (JEP-0078) authentication used in jabberd1.4 and
	transport authentication.
	'''
	def __init__(self, user, password, resource, on_auth):
		''' Caches username, password and resource for auth. '''
		PlugIn.__init__(self)
		self.user = user
		if password is None:
			self.password = ''
		else:
			self.password = password
		self.resource = resource
		self.on_auth = on_auth

	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.
		'''
		log.info('Querying server about possible auth methods')
		self.owner = owner

		owner.Dispatcher.SendAndWaitForResponse(
			Iq('get', NS_AUTH, payload=[Node('username', payload=[self.user])]),
			func=self._on_username)

	def _on_username(self, resp):
		if not isResultNode(resp):
			log.error('No result node arrived! Aborting...')
			return self.on_auth(None)

		iq=Iq(typ='set',node=resp)
		query = iq.getTag('query')
		query.setTagData('username',self.user)
		query.setTagData('resource',self.resource)

		if query.getTag('digest'):
			log.info("Performing digest authentication")
			query.setTagData('digest',
				hashlib.sha1(self.owner.Dispatcher.Stream._document_attrs['id']
				+ self.password).hexdigest())
			if query.getTag('password'):
				query.delChild('password')
			self._method = 'digest'
		elif query.getTag('token'):
			token = query.getTagData('token')
			seq = query.getTagData('sequence')
			log.info("Performing zero-k authentication")

			def hasher(s):
				return hashlib.sha1(s).hexdigest()

			def hash_n_times(s, count):
				return count and hasher(hash_n_times(s, count-1)) or s

			hash_ = hash_n_times(hasher(hasher(self.password) + token), int(seq))
			query.setTagData('hash', hash_)
			self._method='0k'
		else:
			log.warn("Sequre methods unsupported, performing plain text \
				authentication")
			query.setTagData('password', self.password)
			self._method = 'plain'
		resp = self.owner.Dispatcher.SendAndWaitForResponse(iq,func=self._on_auth)

	def _on_auth(self, resp):
		if isResultNode(resp):
			log.info('Sucessfully authenticated with remote host.')
			self.owner.User = self.user
			self.owner.Resource = self.resource
			self.owner._registered_name = self.owner.User+'@'+self.owner.Server+\
				'/'+self.owner.Resource
			return self.on_auth(self._method)
		log.error('Authentication failed!')
		return self.on_auth(None)


class NonBlockingBind(PlugIn):
	'''
	Bind some JID to the current connection to allow router know of our
	location. Must be plugged after successful SASL auth.
	'''
	def __init__(self):
		PlugIn.__init__(self)
		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 FeaturesHandler(self, conn, feats):
		'''
		Determine if server supports resource binding and set some internal
		attributes accordingly.
		'''
		if not feats.getTag('bind', namespace=NS_BIND):
			log.error('Server does not requested binding.')
			# we try to bind resource anyway
			#self.bound='failure'
			self.bound = []
			return
		if feats.getTag('session', namespace=NS_SESSION):
			self.session = 1
		else:
			self.session = -1
		self.bound = []

	def plugout(self):
		''' Remove Bind handler from owner's dispatcher. Used internally. '''
		self._owner.UnregisterHandler('features', self.FeaturesHandler,
			xmlns=NS_STREAMS)

	def NonBlockingBind(self, resource=None, on_bound=None):
		''' Perform binding.
		Use provided resource name or random (if not provided).
		'''
		self.on_bound = on_bound
		self._resource = resource
		if self._resource:
			self._resource = [Node('resource', payload=[self._resource])]
		else:
			self._resource = []

		self._owner.onreceive(None)
		self._owner.Dispatcher.SendAndWaitForResponse(
			Protocol('iq',typ='set', payload=[Node('bind', attrs={'xmlns':NS_BIND},
			payload=self._resource)]), func=self._on_bound)

	def _on_bound(self, resp):
		if isResultNode(resp):
			if resp.getTag('bind') and resp.getTag('bind').getTagData('jid'):
				self.bound.append(resp.getTag('bind').getTagData('jid'))
				log.info('Successfully bound %s.' % self.bound[-1])
				jid = JID(resp.getTag('bind').getTagData('jid'))
				self._owner.User = jid.getNode()
				self._owner.Resource = jid.getResource()
				if hasattr(self, 'session') and self.session == -1:
					# Server don't want us to initialize a session
					log.info('No session required.')
					self.on_bound('ok')
				else:
					self._owner.SendAndWaitForResponse(Protocol('iq', typ='set',
						payload=[Node('session', attrs={'xmlns':NS_SESSION})]),
						func=self._on_session)
				return
		if resp:
			log.error('Binding failed: %s.' % resp.getTag('error'))
			self.on_bound(None)
		else:
			log.error('Binding failed: timeout expired.')
			self.on_bound(None)

	def _on_session(self, resp):
		self._owner.onreceive(None)
		if isResultNode(resp):
			log.info('Successfully opened session.')
			self.session = 1
			self.on_bound('ok')
		else:
			log.error('Session open failed.')
			self.session = 0
			self.on_bound(None)

# vim: se ts=3:
