Package pyxmpp :: Package jabber :: Module clientstream
[hide private]

Source Code for Module pyxmpp.jabber.clientstream

  1  # 
  2  # (C) Copyright 2003-2006 Jacek Konieczny <jajcus@jajcus.net> 
  3  # 
  4  # This program is free software; you can redistribute it and/or modify 
  5  # it under the terms of the GNU Lesser General Public License Version 
  6  # 2.1 as published by the Free Software Foundation. 
  7  # 
  8  # This program is distributed in the hope that it will be useful, 
  9  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 10  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 11  # GNU Lesser General Public License for more details. 
 12  # 
 13  # You should have received a copy of the GNU Lesser General Public 
 14  # License along with this program; if not, write to the Free Software 
 15  # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 
 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   
43 -class LegacyClientStream(ClientStream):
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
80 - def _reset(self):
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
113 - def _try_auth(self):
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
146 - def auth_in_stage1(self,stanza):
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
171 - def auth_in_stage2(self,stanza):
172 """Handle the second stage (<iq type='set'/>) of legacy ("plain" or 173 "digest") authentication. 174 175 [server only]""" 176 self.lock.acquire() 177 try: 178 if "plain" not in self.auth_methods and "digest" not in self.auth_methods: 179 iq=stanza.make_error_response("not-allowed") 180 self.send(iq) 181 return 182 183 username=stanza.xpath_eval("a:query/a:username",{"a":"jabber:iq:auth"}) 184 if username: 185 username=from_utf8(username[0].getContent()) 186 resource=stanza.xpath_eval("a:query/a:resource",{"a":"jabber:iq:auth"}) 187 if resource: 188 resource=from_utf8(resource[0].getContent()) 189 if not username or not resource: 190 self.__logger.debug("No username or resource found in auth request") 191 iq=stanza.make_error_response("bad-request") 192 self.send(iq) 193 return 194 195 if stanza.xpath_eval("a:query/a:password",{"a":"jabber:iq:auth"}): 196 if "plain" not in self.auth_methods: 197 iq=stanza.make_error_response("not-allowed") 198 self.send(iq) 199 return 200 else: 201 return self._plain_auth_in_stage2(username,resource,stanza) 202 if stanza.xpath_eval("a:query/a:digest",{"a":"jabber:iq:auth"}): 203 if "plain" not in self.auth_methods: 204 iq=stanza.make_error_response("not-allowed") 205 self.send(iq) 206 return 207 else: 208 return self._digest_auth_in_stage2(username,resource,stanza) 209 finally: 210 self.lock.release()
211
212 - def _auth_stage1(self):
213 """Do the first stage (<iq type='get'/>) of legacy ("plain" or 214 "digest") authentication. 215 216 [client only]""" 217 iq=Iq(stanza_type="get") 218 q=iq.new_query("jabber:iq:auth") 219 q.newTextChild(None,"username",to_utf8(self.my_jid.node)) 220 q.newTextChild(None,"resource",to_utf8(self.my_jid.resource)) 221 self.send(iq) 222 self.set_response_handlers(iq,self.auth_stage2,self.auth_error, 223 self.auth_timeout,timeout=60) 224 iq.free()
225
226 - def auth_timeout(self):
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
238 - def auth_error(self,stanza):
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
255 - def auth_stage2(self,stanza):
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
273 - def _plain_auth_stage2(self, _unused):
274 """Do the second stage (<iq type='set'/>) of legacy "plain" 275 authentication. 276 277 [client only]""" 278 iq=Iq(stanza_type="set") 279 q=iq.new_query("jabber:iq:auth") 280 q.newTextChild(None,"username",to_utf8(self.my_jid.node)) 281 q.newTextChild(None,"resource",to_utf8(self.my_jid.resource)) 282 q.newTextChild(None,"password",to_utf8(self.password)) 283 self.send(iq) 284 self.set_response_handlers(iq,self.auth_finish,self.auth_error) 285 iq.free()
286
287 - def _plain_auth_in_stage2(self, username, _unused, stanza):
288 """Handle the second stage (<iq type='set'/>) of legacy "plain" 289 authentication. 290 291 [server only]""" 292 password=stanza.xpath_eval("a:query/a:password",{"a":"jabber:iq:auth"}) 293 if password: 294 password=from_utf8(password[0].getContent()) 295 if not password: 296 self.__logger.debug("No password found in plain auth request") 297 iq=stanza.make_error_response("bad-request") 298 self.send(iq) 299 return 300 301 if self.check_password(username,password): 302 iq=stanza.make_result_response() 303 self.send(iq) 304 self.peer_authenticated=True 305 self.auth_method_used="plain" 306 self.state_change("authorized",self.peer) 307 self._post_auth() 308 else: 309 self.__logger.debug("Plain auth failed") 310 iq=stanza.make_error_response("bad-request") 311 e=iq.get_error() 312 e.add_custom_condition('jabber:iq:auth:error',"user-unauthorized") 313 self.send(iq)
314
315 - def _digest_auth_stage2(self, _unused):
316 """Do the second stage (<iq type='set'/>) of legacy "digest" 317 authentication. 318 319 [client only]""" 320 iq=Iq(stanza_type="set") 321 q=iq.new_query("jabber:iq:auth") 322 q.newTextChild(None,"username",to_utf8(self.my_jid.node)) 323 q.newTextChild(None,"resource",to_utf8(self.my_jid.resource)) 324 325 digest = sha_factory(to_utf8(self.stream_id)+to_utf8(self.password)).hexdigest() 326 327 q.newTextChild(None,"digest",digest) 328 self.send(iq) 329 self.set_response_handlers(iq,self.auth_finish,self.auth_error) 330 iq.free()
331
332 - def _digest_auth_in_stage2(self, username, _unused, stanza):
333 """Handle the second stage (<iq type='set'/>) of legacy "digest" 334 authentication. 335 336 [server only]""" 337 digest=stanza.xpath_eval("a:query/a:digest",{"a":"jabber:iq:auth"}) 338 if digest: 339 digest=digest[0].getContent() 340 if not digest: 341 self.__logger.debug("No digest found in digest auth request") 342 iq=stanza.make_error_response("bad-request") 343 self.send(iq) 344 return 345 346 password,pwformat=self.get_password(username) 347 if not password or pwformat!="plain": 348 iq=stanza.make_error_response("bad-request") 349 e=iq.get_error() 350 e.add_custom_condition('jabber:iq:auth:error',"user-unauthorized") 351 self.send(iq) 352 return 353 354 mydigest = sha_factory(to_utf8(self.stream_id)+to_utf8(password)).hexdigest() 355 356 if mydigest==digest: 357 iq=stanza.make_result_response() 358 self.send(iq) 359 self.peer_authenticated=True 360 self.auth_method_used="digest" 361 self.state_change("authorized",self.peer) 362 self._post_auth() 363 else: 364 self.__logger.debug("Digest auth failed: %r != %r" % (digest,mydigest)) 365 iq=stanza.make_error_response("bad-request") 366 e=iq.get_error() 367 e.add_custom_condition('jabber:iq:auth:error',"user-unauthorized") 368 self.send(iq)
369
370 - def auth_finish(self, _unused):
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
381 - def registration_error(self, stanza):
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
402 - def registration_form_received(self, stanza):
403 """Handle registration form received. 404 405 [client only] 406 407 Call self.registration_callback with the registration form received 408 as the argument. Use the value returned by the callback will be a 409 filled-in form. 410 411 :Parameters: 412 - `stanza`: the stanza received. 413 :Types: 414 - `stanza`: `pyxmpp.iq.Iq`""" 415 self.lock.acquire() 416 try: 417 self.__register = Register(stanza.get_query()) 418 self.registration_callback(stanza, self.__register.get_form()) 419 finally: 420 self.lock.release()
421
422 - def submit_registration_form(self, form):
423 """Submit a registration form. 424 425 [client only] 426 427 :Parameters: 428 - `form`: the filled-in form. When form is `None` or its type is 429 "cancel" the registration is to be canceled. 430 431 :Types: 432 - `form`: `pyxmpp.jabber.dataforms.Form`""" 433 self.lock.acquire() 434 try: 435 if form and form.type!="cancel": 436 self.registration_form = form 437 iq = Iq(stanza_type = "set") 438 iq.set_content(self.__register.submit_form(form)) 439 self.set_response_handlers(iq, self.registration_success, self.registration_error) 440 self.send(iq) 441 else: 442 self.__register = None 443 finally: 444 self.lock.release()
445
446 - def registration_success(self, stanza):
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 # vi: sts=4 et sw=4 475