auth.py 15 KB

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