sigh
[uccdoor.git] / xmpp / auth.py
1 ##   auth.py
2 ##
3 ##   Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov
4 ##
5 ##   This program is free software; you can redistribute it and/or modify
6 ##   it under the terms of the GNU General Public License as published by
7 ##   the Free Software Foundation; either version 2, or (at your option)
8 ##   any later version.
9 ##
10 ##   This program is distributed in the hope that it will be useful,
11 ##   but WITHOUT ANY WARRANTY; without even the implied warranty of
12 ##   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 ##   GNU General Public License for more details.
14
15 # $Id: auth.py,v 1.41 2008/09/13 21:45:21 normanr Exp $
16
17 """
18 Provides library with all Non-SASL and SASL authentication mechanisms.
19 Can be used both for client and transport authentication.
20 """
21
22 from protocol import *
23 from client import PlugIn
24 import sha,base64,random,dispatcher,re
25
26 import md5
27 def HH(some): return md5.new(some).hexdigest()
28 def H(some): return md5.new(some).digest()
29 def C(some): return ':'.join(some)
30
31 class NonSASL(PlugIn):
32     """ Implements old Non-SASL (JEP-0078) authentication used in jabberd1.4 and transport authentication."""
33     def __init__(self,user,password,resource):
34         """ Caches username, password and resource for auth. """
35         PlugIn.__init__(self)
36         self.DBG_LINE='gen_auth'
37         self.user=user
38         self.password=password
39         self.resource=resource
40
41     def plugin(self,owner):
42         """ Determine the best auth method (digest/0k/plain) and use it for auth.
43             Returns used method name on success. Used internally. """
44         if not self.resource: return self.authComponent(owner)
45         self.DEBUG('Querying server about possible auth methods','start')
46         resp=owner.Dispatcher.SendAndWaitForResponse(Iq('get',NS_AUTH,payload=[Node('username',payload=[self.user])]))
47         if not isResultNode(resp):
48             self.DEBUG('No result node arrived! Aborting...','error')
49             return
50         iq=Iq(typ='set',node=resp)
51         query=iq.getTag('query')
52         query.setTagData('username',self.user)
53         query.setTagData('resource',self.resource)
54
55         if query.getTag('digest'):
56             self.DEBUG("Performing digest authentication",'ok')
57             query.setTagData('digest',sha.new(owner.Dispatcher.Stream._document_attrs['id']+self.password).hexdigest())
58             if query.getTag('password'): query.delChild('password')
59             method='digest'
60         elif query.getTag('token'):
61             token=query.getTagData('token')
62             seq=query.getTagData('sequence')
63             self.DEBUG("Performing zero-k authentication",'ok')
64             hash = sha.new(sha.new(self.password).hexdigest()+token).hexdigest()
65             for foo in xrange(int(seq)): hash = sha.new(hash).hexdigest()
66             query.setTagData('hash',hash)
67             method='0k'
68         else:
69             self.DEBUG("Sequre methods unsupported, performing plain text authentication",'warn')
70             query.setTagData('password',self.password)
71             method='plain'
72         resp=owner.Dispatcher.SendAndWaitForResponse(iq)
73         if isResultNode(resp):
74             self.DEBUG('Sucessfully authenticated with remove host.','ok')
75             owner.User=self.user
76             owner.Resource=self.resource
77             owner._registered_name=owner.User+'@'+owner.Server+'/'+owner.Resource
78             return method
79         self.DEBUG('Authentication failed!','error')
80
81     def authComponent(self,owner):
82         """ Authenticate component. Send handshake stanza and wait for result. Returns "ok" on success. """
83         self.handshake=0
84         owner.send(Node(NS_COMPONENT_ACCEPT+' handshake',payload=[sha.new(owner.Dispatcher.Stream._document_attrs['id']+self.password).hexdigest()]))
85         owner.RegisterHandler('handshake',self.handshakeHandler,xmlns=NS_COMPONENT_ACCEPT)
86         while not self.handshake:
87             self.DEBUG("waiting on handshake",'notify')
88             owner.Process(1)
89         owner._registered_name=self.user
90         if self.handshake+1: return 'ok'
91
92     def handshakeHandler(self,disp,stanza):
93         """ Handler for registering in dispatcher for accepting transport authentication. """
94         if stanza.getName()=='handshake': self.handshake=1
95         else: self.handshake=-1
96
97 class SASL(PlugIn):
98     """ Implements SASL authentication. """
99     def __init__(self,username,password):
100         PlugIn.__init__(self)
101         self.username=username
102         self.password=password
103
104     def plugin(self,owner):
105         if not self._owner.Dispatcher.Stream._document_attrs.has_key('version'): self.startsasl='not-supported'
106         elif self._owner.Dispatcher.Stream.features:
107             try: self.FeaturesHandler(self._owner.Dispatcher,self._owner.Dispatcher.Stream.features)
108             except NodeProcessed: pass
109         else: self.startsasl=None
110
111     def auth(self):
112         """ Start authentication. Result can be obtained via "SASL.startsasl" attribute and will be
113             either "success" or "failure". Note that successfull auth will take at least
114             two Dispatcher.Process() calls. """
115         if self.startsasl: pass
116         elif self._owner.Dispatcher.Stream.features:
117             try: self.FeaturesHandler(self._owner.Dispatcher,self._owner.Dispatcher.Stream.features)
118             except NodeProcessed: pass
119         else: self._owner.RegisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS)
120
121     def plugout(self):
122         """ Remove SASL handlers from owner's dispatcher. Used internally. """
123         if self._owner.__dict__.has_key('features'): self._owner.UnregisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS)
124         if self._owner.__dict__.has_key('challenge'): self._owner.UnregisterHandler('challenge',self.SASLHandler,xmlns=NS_SASL)
125         if self._owner.__dict__.has_key('failure'): self._owner.UnregisterHandler('failure',self.SASLHandler,xmlns=NS_SASL)
126         if self._owner.__dict__.has_key('success'): self._owner.UnregisterHandler('success',self.SASLHandler,xmlns=NS_SASL)
127
128     def FeaturesHandler(self,conn,feats):
129         """ Used to determine if server supports SASL auth. Used internally. """
130         if not feats.getTag('mechanisms',namespace=NS_SASL):
131             self.startsasl='not-supported'
132             self.DEBUG('SASL not supported by server','error')
133             return
134         mecs=[]
135         for mec in feats.getTag('mechanisms',namespace=NS_SASL).getTags('mechanism'):
136             mecs.append(mec.getData())
137         self._owner.RegisterHandler('challenge',self.SASLHandler,xmlns=NS_SASL)
138         self._owner.RegisterHandler('failure',self.SASLHandler,xmlns=NS_SASL)
139         self._owner.RegisterHandler('success',self.SASLHandler,xmlns=NS_SASL)
140         if "ANONYMOUS" in mecs and self.username == None:
141             node=Node('auth',attrs={'xmlns':NS_SASL,'mechanism':'ANONYMOUS'})
142         elif "DIGEST-MD5" in mecs:
143             node=Node('auth',attrs={'xmlns':NS_SASL,'mechanism':'DIGEST-MD5'})
144         elif "PLAIN" in mecs:
145             sasl_data='%s\x00%s\x00%s'%(self.username+'@'+self._owner.Server,self.username,self.password)
146             node=Node('auth',attrs={'xmlns':NS_SASL,'mechanism':'PLAIN'},payload=[base64.encodestring(sasl_data).replace('\r','').replace('\n','')])
147         else:
148             self.startsasl='failure'
149             self.DEBUG('I can only use DIGEST-MD5 and PLAIN mecanisms.','error')
150             return
151         self.startsasl='in-process'
152         self._owner.send(node.__str__())
153         raise NodeProcessed
154
155     def SASLHandler(self,conn,challenge):
156         """ Perform next SASL auth step. Used internally. """
157         if challenge.getNamespace()<>NS_SASL: return
158         if challenge.getName()=='failure':
159             self.startsasl='failure'
160             try: reason=challenge.getChildren()[0]
161             except: reason=challenge
162             self.DEBUG('Failed SASL authentification: %s'%reason,'error')
163             raise NodeProcessed
164         elif challenge.getName()=='success':
165             self.startsasl='success'
166             self.DEBUG('Successfully authenticated with remote server.','ok')
167             handlers=self._owner.Dispatcher.dumpHandlers()
168             self._owner.Dispatcher.PlugOut()
169             dispatcher.Dispatcher().PlugIn(self._owner)
170             self._owner.Dispatcher.restoreHandlers(handlers)
171             self._owner.User=self.username
172             raise NodeProcessed
173 ########################################3333
174         incoming_data=challenge.getData()
175         chal={}
176         data=base64.decodestring(incoming_data)
177         self.DEBUG('Got challenge:'+data,'ok')
178         for pair in re.findall('(\w+\s*=\s*(?:(?:"[^"]+")|(?:[^,]+)))',data):
179             key,value=[x.strip() for x in pair.split('=', 1)]
180             if value[:1]=='"' and value[-1:]=='"': value=value[1:-1]
181             chal[key]=value
182         if chal.has_key('qop') and 'auth' in [x.strip() for x in chal['qop'].split(',')]:
183             resp={}
184             resp['username']=self.username
185             resp['realm']=self._owner.Server
186             resp['nonce']=chal['nonce']
187             cnonce=''
188             for i in range(7):
189                 cnonce+=hex(int(random.random()*65536*4096))[2:]
190             resp['cnonce']=cnonce
191             resp['nc']=('00000001')
192             resp['qop']='auth'
193             resp['digest-uri']='xmpp/'+self._owner.Server
194             A1=C([H(C([resp['username'],resp['realm'],self.password])),resp['nonce'],resp['cnonce']])
195             A2=C(['AUTHENTICATE',resp['digest-uri']])
196             response= HH(C([HH(A1),resp['nonce'],resp['nc'],resp['cnonce'],resp['qop'],HH(A2)]))
197             resp['response']=response
198             resp['charset']='utf-8'
199             sasl_data=''
200             for key in ['charset','username','realm','nonce','nc','cnonce','digest-uri','response','qop']:
201                 if key in ['nc','qop','response','charset']: sasl_data+="%s=%s,"%(key,resp[key])
202                 else: sasl_data+='%s="%s",'%(key,resp[key])
203 ########################################3333
204             node=Node('response',attrs={'xmlns':NS_SASL},payload=[base64.encodestring(sasl_data[:-1]).replace('\r','').replace('\n','')])
205             self._owner.send(node.__str__())
206         elif chal.has_key('rspauth'): self._owner.send(Node('response',attrs={'xmlns':NS_SASL}).__str__())
207         else: 
208             self.startsasl='failure'
209             self.DEBUG('Failed SASL authentification: unknown challenge','error')
210         raise NodeProcessed
211
212 class Bind(PlugIn):
213     """ Bind some JID to the current connection to allow router know of our location."""
214     def __init__(self):
215         PlugIn.__init__(self)
216         self.DBG_LINE='bind'
217         self.bound=None
218
219     def plugin(self,owner):
220         """ Start resource binding, if allowed at this time. Used internally. """
221         if self._owner.Dispatcher.Stream.features:
222             try: self.FeaturesHandler(self._owner.Dispatcher,self._owner.Dispatcher.Stream.features)
223             except NodeProcessed: pass
224         else: self._owner.RegisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS)
225
226     def plugout(self):
227         """ Remove Bind handler from owner's dispatcher. Used internally. """
228         self._owner.UnregisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS)
229
230     def FeaturesHandler(self,conn,feats):
231         """ Determine if server supports resource binding and set some internal attributes accordingly. """
232         if not feats.getTag('bind',namespace=NS_BIND):
233             self.bound='failure'
234             self.DEBUG('Server does not requested binding.','error')
235             return
236         if feats.getTag('session',namespace=NS_SESSION): self.session=1
237         else: self.session=-1
238         self.bound=[]
239
240     def Bind(self,resource=None):
241         """ Perform binding. Use provided resource name or random (if not provided). """
242         while self.bound is None and self._owner.Process(1): pass
243         if resource: resource=[Node('resource',payload=[resource])]
244         else: resource=[]
245         resp=self._owner.SendAndWaitForResponse(Protocol('iq',typ='set',payload=[Node('bind',attrs={'xmlns':NS_BIND},payload=resource)]))
246         if isResultNode(resp):
247             self.bound.append(resp.getTag('bind').getTagData('jid'))
248             self.DEBUG('Successfully bound %s.'%self.bound[-1],'ok')
249             jid=JID(resp.getTag('bind').getTagData('jid'))
250             self._owner.User=jid.getNode()
251             self._owner.Resource=jid.getResource()
252             resp=self._owner.SendAndWaitForResponse(Protocol('iq',typ='set',payload=[Node('session',attrs={'xmlns':NS_SESSION})]))
253             if isResultNode(resp):
254                 self.DEBUG('Successfully opened session.','ok')
255                 self.session=1
256                 return 'ok'
257             else:
258                 self.DEBUG('Session open failed.','error')
259                 self.session=0
260         elif resp: self.DEBUG('Binding failed: %s.'%resp.getTag('error'),'error')
261         else:
262             self.DEBUG('Binding failed: timeout expired.','error')
263             return ''
264
265 class ComponentBind(PlugIn):
266     """ ComponentBind some JID to the current connection to allow router know of our location."""
267     def __init__(self, sasl):
268         PlugIn.__init__(self)
269         self.DBG_LINE='bind'
270         self.bound=None
271         self.needsUnregister=None
272         self.sasl = sasl
273
274     def plugin(self,owner):
275         """ Start resource binding, if allowed at this time. Used internally. """
276         if not self.sasl:
277             self.bound=[]
278             return
279         if self._owner.Dispatcher.Stream.features:
280             try: self.FeaturesHandler(self._owner.Dispatcher,self._owner.Dispatcher.Stream.features)
281             except NodeProcessed: pass
282         else:
283             self._owner.RegisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS)
284             self.needsUnregister=1
285
286     def plugout(self):
287         """ Remove ComponentBind handler from owner's dispatcher. Used internally. """
288         if self.needsUnregister:
289             self._owner.UnregisterHandler('features',self.FeaturesHandler,xmlns=NS_STREAMS)
290
291     def FeaturesHandler(self,conn,feats):
292         """ Determine if server supports resource binding and set some internal attributes accordingly. """
293         if not feats.getTag('bind',namespace=NS_BIND):
294             self.bound='failure'
295             self.DEBUG('Server does not requested binding.','error')
296             return
297         if feats.getTag('session',namespace=NS_SESSION): self.session=1
298         else: self.session=-1
299         self.bound=[]
300
301     def Bind(self,domain=None):
302         """ Perform binding. Use provided domain name (if not provided). """
303         while self.bound is None and self._owner.Process(1): pass
304         if self.sasl:
305             xmlns = NS_COMPONENT_1
306         else:
307             xmlns = None
308         self.bindresponse = None
309         ttl = dispatcher.DefaultTimeout
310         self._owner.RegisterHandler('bind',self.BindHandler,xmlns=xmlns)
311         self._owner.send(Protocol('bind',attrs={'name':domain},xmlns=NS_COMPONENT_1))
312         while self.bindresponse is None and self._owner.Process(1) and ttl > 0: ttl-=1
313         self._owner.UnregisterHandler('bind',self.BindHandler,xmlns=xmlns)
314         resp=self.bindresponse
315         if resp and resp.getAttr('error'):
316             self.DEBUG('Binding failed: %s.'%resp.getAttr('error'),'error')
317         elif resp:
318             self.DEBUG('Successfully bound.','ok')
319             return 'ok'
320         else:
321             self.DEBUG('Binding failed: timeout expired.','error')
322             return ''
323
324     def BindHandler(self,conn,bind):
325         self.bindresponse = bind
326         pass

UCC git Repository :: git.ucc.asn.au