##   auth_nb.py
##       based on auth.py
##
##   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 library with all Non-SASL and SASL authentication mechanisms.
Can be used both for client and transport authentication.
'''
import sys
from protocol import *
from auth import *
from client import PlugIn
import sha,base64,random,dispatcher_nb

def challenge_splitter(data):
	''' Helper function that creates a dict from challenge string.
	Sample chalenge string:
		username="example.org",realm="somerealm",\
		nonce="OA6MG9tEQGm2hh",cnonce="OA6MHXh6VqTrRk",\
		nc=00000001,qop="auth,auth-int,auth-conf",charset=utf-8
	in the above example:
		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. '''
	def __init__(self,username,password, on_sasl):
		PlugIn.__init__(self)
		self.username=username
		self.password=password
		self.on_sasl = on_sasl
		self.realm = None
	def plugin(self,owner):
		if not self._owner.Dispatcher.Stream._document_attrs.has_key('version'): 
			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. '''
		self._owner.UnregisterHandler('features', self.FeaturesHandler, xmlns=NS_STREAMS)
		self._owner.UnregisterHandler('challenge', self.SASLHandler, xmlns=NS_SASL)
		self._owner.UnregisterHandler('failure', self.SASLHandler, xmlns=NS_SASL)
		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=[]
		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 "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('\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')
			if self.on_sasl :
				self.on_sasl ()
			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_nb.Dispatcher().PlugIn(self._owner)
			self._owner.Dispatcher.restoreHandlers(handlers)
			self._owner.User = self.username
			if self.on_sasl :
				self.on_sasl ()
			raise NodeProcessed
########################################3333
		incoming_data = challenge.getData()
		data=base64.decodestring(incoming_data)
		self.DEBUG('Got challenge:'+data,'ok')
		chal = challenge_splitter(data)
		if not self.realm and chal.has_key('realm'):
			self.realm = chal['realm']
		if chal.has_key('qop') and ((type(chal['qop']) == str and \
		chal['qop'] =='auth') or (type(chal['qop']) == list and 'auth' in \
		chal['qop'])):
			resp={}
			resp['username'] = self.username
			if self.realm:
				resp['realm'] = self.realm
			else:
				resp['realm'] = self._owner.Server
			resp['nonce']=chal['nonce']
			cnonce=''
			for i in range(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 ['charset', 'username', 'realm', 'nonce', 'nc', 'cnonce', 'digest-uri', 'response', 'qop']:
				if key in ['nc','qop','response','charset']: 
					sasl_data += "%s=%s," % (key,resp[key])
				else: 
					sasl_data += '%s="%s",' % (key,resp[key])
########################################3333
			node=Node('response', attrs={'xmlns':NS_SASL}, 
				payload=[base64.encodestring(sasl_data[:-1]).replace('\r','').replace('\n','')])
			self._owner.send(node.__str__())
		elif chal.has_key('rspauth'): 
			self._owner.send(Node('response', attrs={'xmlns':NS_SASL}).__str__())
		else: 
			self.startsasl='failure'
			self.DEBUG('Failed SASL authentification: unknown challenge', 'error')
		if self.on_sasl :
				self.on_sasl ()
		raise NodeProcessed
	
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.DBG_LINE ='gen_auth'
		self.user = user
		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. '''
		if not self.resource: 
			return self.authComponent(owner)
		self.DEBUG('Querying server about possible auth methods', 'start')
		self.owner = owner 
		
		resp = 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):
			self.DEBUG('No result node arrived! Aborting...','error')
			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'):
			self.DEBUG("Performing digest authentication",'ok')
			query.setTagData('digest', 
				sha.new(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')
			self.DEBUG("Performing zero-k authentication",'ok')
			hash = sha.new(sha.new(self.password).hexdigest()+token).hexdigest()
			for foo in xrange(int(seq)): 
				hash = sha.new(hash).hexdigest()
			query.setTagData('hash',hash)
			self._method='0k'
		else:
			self.DEBUG("Sequre methods unsupported, performing plain text authentication",'warn')
			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):
			self.DEBUG('Sucessfully authenticated with remove host.','ok')
			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)
		self.DEBUG('Authentication failed!','error')
		return self.on_auth(None)

	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=[sha.new(owner.Dispatcher.Stream._document_attrs['id']+self.password).hexdigest()]))
		owner.RegisterHandler('handshake', self.handshakeHandler, xmlns=NS_COMPONENT_ACCEPT)
		self._owner.onreceive(self._on_auth_component)
		
	def _on_auth_component(self, data):
		''' called when we receive some response, after we send the handshake '''
		if data:
			self.Dispatcher.ProcessNonBlocking(data)
		if not self.handshake:
			self.DEBUG('waiting on handshake', 'notify')
			return
		self._owner.onreceive(None)
		owner._registered_name=self.user
		if self.handshake+1: 
			return self.on_auth('ok')
		self.on_auth(None)

	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 NonBlockingBind(Bind):
	''' Bind some JID to the current connection to allow router know of our location.'''
	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 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)
		resp=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):
			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()
			self._owner.SendAndWaitForResponse(Protocol('iq', typ='set', 
				payload=[Node('session', attrs={'xmlns':NS_SESSION})]), func=self._on_session)
		elif resp:
			self.DEBUG('Binding failed: %s.' % resp.getTag('error'),'error')
			self.on_bound(None)
		else:
			self.DEBUG('Binding failed: timeout expired.', 'error')
			self.on_bound(None)
			
	def _on_session(self, resp):
		self._owner.onreceive(None)
		if isResultNode(resp):
			self.DEBUG('Successfully opened session.', 'ok')
			self.session = 1
			self.on_bound('ok')
		else:
			self.DEBUG('Session open failed.', 'error')
			self.session = 0
			self.on_bound(None)
		self._owner.onreceive(None)
		if isResultNode(resp):
			self.DEBUG('Successfully opened session.', 'ok')
			self.session = 1
			self.on_bound('ok')
		else:
			self.DEBUG('Session open failed.', 'error')
			self.session = 0
			self.on_bound(None)

class NBComponentBind(ComponentBind):
	''' ComponentBind some JID to the current connection to allow 
	router know of our location.
	'''
	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)
			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 Bind(self, domain = None, on_bind = None):
		''' Perform binding. Use provided domain name (if not provided). '''
		self._owner.onreceive(self._on_bound)
		self.on_bind = on_bind
	
	def _on_bound(self, resp):
		if data:
			self.Dispatcher.ProcessNonBlocking(data)
		if self.bound is None:
			return
		self._owner.onreceive(None)
		self._owner.SendAndWaitForResponse(
			Protocol('bind', attrs={'name':domain}, xmlns=NS_COMPONENT_1), 
			func=self._on_bind_reponse)
	
	def _on_bind_reponse(self, res):
		if resp and resp.getAttr('error'):
			self.DEBUG('Binding failed: %s.' % resp.getAttr('error'), 'error')
		elif resp:
			self.DEBUG('Successfully bound.', 'ok')
			if self.on_bind:
				self.on_bind('ok')
		else:
			self.DEBUG('Binding failed: timeout expired.', 'error')
		if self.on_bind:
			self.on_bind(None)
