1   
  2   
  3   
  4   
  5   
  6   
  7   
  8   
  9   
 10   
 11   
 12   
 13   
 14   
 15   
 16   
 17  """XMPP stream support with fallback to legacy non-SASL Jabber authentication. 
 18   
 19  Normative reference: 
 20    - `JEP 78 <http://www.jabber.org/jeps/jep-0078.html>`__ 
 21  """ 
 22   
 23  __revision__="$Id: clientstream.py 683 2008-12-05 18:25:45Z jajcus $" 
 24  __docformat__="restructuredtext en" 
 25   
 26  try: 
 27      import hashlib 
 28      sha_factory = hashlib.sha1 
 29  except ImportError: 
 30      import sha 
 31      sha_factory = sha.new 
 32   
 33  import logging 
 34   
 35  from pyxmpp.iq import Iq 
 36  from pyxmpp.utils import to_utf8,from_utf8 
 37  from pyxmpp.jid import JID 
 38  from pyxmpp.clientstream import ClientStream 
 39  from pyxmpp.jabber.register import Register 
 40   
 41  from pyxmpp.exceptions import ClientStreamError, LegacyAuthenticationError, RegistrationError 
 42   
 44      """Handles Jabber (both XMPP and legacy protocol) client connection stream. 
 45   
 46      Both client and server side of the connection is supported. This class handles 
 47      client SASL and legacy authentication, authorisation and XMPP resource binding. 
 48      """ 
 49 -    def __init__(self, jid, password = None, server = None, port = 5222, 
 50              auth_methods = ("sasl:DIGEST-MD5", "digest"), 
 51              tls_settings = None, keepalive = 0, owner = None): 
  52          """Initialize a LegacyClientStream object. 
 53   
 54          :Parameters: 
 55              - `jid`: local JID. 
 56              - `password`: user's password. 
 57              - `server`: server to use. If not given then address will be derived form the JID. 
 58              - `port`: port number to use. If not given then address will be derived form the JID. 
 59              - `auth_methods`: sallowed authentication methods. SASL authentication mechanisms 
 60                in the list should be prefixed with "sasl:" string. 
 61              - `tls_settings`: settings for StartTLS -- `TLSSettings` instance. 
 62              - `keepalive`: keepalive output interval. 0 to disable. 
 63              - `owner`: `Client`, `Component` or similar object "owning" this stream. 
 64          :Types: 
 65              - `jid`: `pyxmpp.JID` 
 66              - `password`: `unicode` 
 67              - `server`: `unicode` 
 68              - `port`: `int` 
 69              - `auth_methods`: sequence of `str` 
 70              - `tls_settings`: `pyxmpp.TLSSettings` 
 71              - `keepalive`: `int` 
 72          """ 
 73          (self.authenticated, self.available_auth_methods, self.auth_stanza, 
 74                  self.peer_authenticated, self.auth_method_used, 
 75                  self.registration_callback, self.registration_form, self.__register) = (None,) * 8 
 76          ClientStream.__init__(self, jid, password, server, port, 
 77                              auth_methods, tls_settings, keepalive, owner) 
 78          self.__logger=logging.getLogger("pyxmpp.jabber.LegacyClientStream") 
  79   
 81          """Reset the `LegacyClientStream` object state, making the object ready 
 82          to handle new connections.""" 
 83          ClientStream._reset(self) 
 84          self.available_auth_methods = None 
 85          self.auth_stanza = None 
 86          self.registration_callback = None 
  87   
 88 -    def _post_connect(self): 
  89          """Initialize authentication when the connection is established 
 90          and we are the initiator.""" 
 91          if not self.initiator: 
 92              if "plain" in self.auth_methods or "digest" in self.auth_methods: 
 93                  self.set_iq_get_handler("query","jabber:iq:auth", 
 94                              self.auth_in_stage1) 
 95                  self.set_iq_set_handler("query","jabber:iq:auth", 
 96                              self.auth_in_stage2) 
 97          elif self.registration_callback: 
 98              iq = Iq(stanza_type = "get") 
 99              iq.set_content(Register()) 
100              self.set_response_handlers(iq, self.registration_form_received, self.registration_error) 
101              self.send(iq) 
102              return 
103          ClientStream._post_connect(self) 
 104   
105 -    def _post_auth(self): 
 106          """Unregister legacy authentication handlers after successfull 
107          authentication.""" 
108          ClientStream._post_auth(self) 
109          if not self.initiator: 
110              self.unset_iq_get_handler("query","jabber:iq:auth") 
111              self.unset_iq_set_handler("query","jabber:iq:auth") 
 112   
114          """Try to authenticate using the first one of allowed authentication 
115          methods left. 
116   
117          [client only]""" 
118          if self.authenticated: 
119              self.__logger.debug("try_auth: already authenticated") 
120              return 
121          self.__logger.debug("trying auth: %r" % (self._auth_methods_left,)) 
122          if not self._auth_methods_left: 
123              raise LegacyAuthenticationError,"No allowed authentication methods available" 
124          method=self._auth_methods_left[0] 
125          if method.startswith("sasl:"): 
126              return ClientStream._try_auth(self) 
127          elif method not in ("plain","digest"): 
128              self._auth_methods_left.pop(0) 
129              self.__logger.debug("Skipping unknown auth method: %s" % method) 
130              return self._try_auth() 
131          elif self.available_auth_methods is not None: 
132              if method in self.available_auth_methods: 
133                  self._auth_methods_left.pop(0) 
134                  self.auth_method_used=method 
135                  if method=="digest": 
136                      self._digest_auth_stage2(self.auth_stanza) 
137                  else: 
138                      self._plain_auth_stage2(self.auth_stanza) 
139                  self.auth_stanza=None 
140                  return 
141              else: 
142                  self.__logger.debug("Skipping unavailable auth method: %s" % method) 
143          else: 
144              self._auth_stage1() 
 145   
147          """Handle the first stage (<iq type='get'/>) of legacy ("plain" or 
148          "digest") authentication. 
149   
150          [server only]""" 
151          self.lock.acquire() 
152          try: 
153              if "plain" not in self.auth_methods and "digest" not in self.auth_methods: 
154                  iq=stanza.make_error_response("not-allowed") 
155                  self.send(iq) 
156                  return 
157   
158              iq=stanza.make_result_response() 
159              q=iq.new_query("jabber:iq:auth") 
160              q.newChild(None,"username",None) 
161              q.newChild(None,"resource",None) 
162              if "plain" in self.auth_methods: 
163                  q.newChild(None,"password",None) 
164              if "digest" in self.auth_methods: 
165                  q.newChild(None,"digest",None) 
166              self.send(iq) 
167              iq.free() 
168          finally: 
169              self.lock.release() 
 170   
211   
225   
227          """Handle legacy authentication timeout. 
228   
229          [client only]""" 
230          self.lock.acquire() 
231          try: 
232              self.__logger.debug("Timeout while waiting for jabber:iq:auth result") 
233              if self._auth_methods_left: 
234                  self._auth_methods_left.pop(0) 
235          finally: 
236              self.lock.release() 
 237   
239          """Handle legacy authentication error. 
240   
241          [client only]""" 
242          self.lock.acquire() 
243          try: 
244              err=stanza.get_error() 
245              ae=err.xpath_eval("e:*",{"e":"jabber:iq:auth:error"}) 
246              if ae: 
247                  ae=ae[0].name 
248              else: 
249                  ae=err.get_condition().name 
250              raise LegacyAuthenticationError,("Authentication error condition: %s" 
251                          % (ae,)) 
252          finally: 
253              self.lock.release() 
 254   
256          """Handle the first stage authentication response (result of the <iq 
257          type="get"/>). 
258   
259          [client only]""" 
260          self.lock.acquire() 
261          try: 
262              self.__logger.debug("Procesing auth response...") 
263              self.available_auth_methods=[] 
264              if (stanza.xpath_eval("a:query/a:digest",{"a":"jabber:iq:auth"}) and self.stream_id): 
265                  self.available_auth_methods.append("digest") 
266              if (stanza.xpath_eval("a:query/a:password",{"a":"jabber:iq:auth"})): 
267                  self.available_auth_methods.append("plain") 
268              self.auth_stanza=stanza.copy() 
269              self._try_auth() 
270          finally: 
271              self.lock.release() 
 272   
286   
314   
331   
369   
371          """Handle success of the legacy authentication.""" 
372          self.lock.acquire() 
373          try: 
374              self.__logger.debug("Authenticated") 
375              self.authenticated=True 
376              self.state_change("authorized",self.my_jid) 
377              self._post_auth() 
378          finally: 
379              self.lock.release() 
 380   
382          """Handle in-band registration error. 
383   
384          [client only] 
385   
386          :Parameters: 
387              - `stanza`: the error stanza received or `None` on timeout. 
388          :Types: 
389              - `stanza`: `pyxmpp.stanza.Stanza`""" 
390          self.lock.acquire() 
391          try: 
392              err=stanza.get_error() 
393              ae=err.xpath_eval("e:*",{"e":"jabber:iq:auth:error"}) 
394              if ae: 
395                  ae=ae[0].name 
396              else: 
397                  ae=err.get_condition().name 
398              raise RegistrationError,("Authentication error condition: %s" % (ae,)) 
399          finally: 
400              self.lock.release() 
 401   
421   
445   
447          """Handle registration success. 
448   
449          [client only] 
450   
451          Clean up registration stuff, change state to "registered" and initialize 
452          authentication. 
453   
454          :Parameters: 
455              - `stanza`: the stanza received. 
456          :Types: 
457              - `stanza`: `pyxmpp.iq.Iq`""" 
458          _unused = stanza 
459          self.lock.acquire() 
460          try: 
461              self.state_change("registered", self.registration_form) 
462              if ('FORM_TYPE' in self.registration_form 
463                      and self.registration_form['FORM_TYPE'].value == 'jabber:iq:register'): 
464                  if 'username' in self.registration_form: 
465                      self.my_jid = JID(self.registration_form['username'].value, 
466                              self.my_jid.domain, self.my_jid.resource) 
467                  if 'password' in self.registration_form: 
468                      self.password = self.registration_form['password'].value 
469              self.registration_callback = None 
470              self._post_connect() 
471          finally: 
472              self.lock.release() 
  473   
474   
475