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

Source Code for Module pyxmpp.jabber.clientstream

  1  # 
  2  # (C) Copyright 2003-2010 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  __docformat__="restructuredtext en" 
 24   
 25  import hashlib 
 26  import logging 
 27   
 28  from pyxmpp.iq import Iq 
 29  from pyxmpp.utils import to_utf8,from_utf8 
 30  from pyxmpp.jid import JID 
 31  from pyxmpp.clientstream import ClientStream 
 32  from pyxmpp.jabber.register import Register 
 33   
 34  from pyxmpp.exceptions import ClientStreamError, LegacyAuthenticationError, RegistrationError 
 35   
36 -class LegacyClientStream(ClientStream):
37 """Handles Jabber (both XMPP and legacy protocol) client connection stream. 38 39 Both client and server side of the connection is supported. This class handles 40 client SASL and legacy authentication, authorisation and XMPP resource binding. 41 """
42 - def __init__(self, jid, password = None, server = None, port = 5222, 43 auth_methods = ("sasl:DIGEST-MD5", "digest"), 44 tls_settings = None, keepalive = 0, owner = None):
45 """Initialize a LegacyClientStream object. 46 47 :Parameters: 48 - `jid`: local JID. 49 - `password`: user's password. 50 - `server`: server to use. If not given then address will be derived form the JID. 51 - `port`: port number to use. If not given then address will be derived form the JID. 52 - `auth_methods`: sallowed authentication methods. SASL authentication mechanisms 53 in the list should be prefixed with "sasl:" string. 54 - `tls_settings`: settings for StartTLS -- `TLSSettings` instance. 55 - `keepalive`: keepalive output interval. 0 to disable. 56 - `owner`: `Client`, `Component` or similar object "owning" this stream. 57 :Types: 58 - `jid`: `pyxmpp.JID` 59 - `password`: `unicode` 60 - `server`: `unicode` 61 - `port`: `int` 62 - `auth_methods`: sequence of `str` 63 - `tls_settings`: `pyxmpp.TLSSettings` 64 - `keepalive`: `int` 65 """ 66 (self.authenticated, self.available_auth_methods, self.auth_stanza, 67 self.peer_authenticated, self.auth_method_used, 68 self.registration_callback, self.registration_form, self.__register) = (None,) * 8 69 ClientStream.__init__(self, jid, password, server, port, 70 auth_methods, tls_settings, keepalive, owner) 71 self.__logger=logging.getLogger("pyxmpp.jabber.LegacyClientStream")
72
73 - def _reset(self):
74 """Reset the `LegacyClientStream` object state, making the object ready 75 to handle new connections.""" 76 ClientStream._reset(self) 77 self.available_auth_methods = None 78 self.auth_stanza = None 79 self.registration_callback = None
80
81 - def _post_connect(self):
82 """Initialize authentication when the connection is established 83 and we are the initiator.""" 84 if not self.initiator: 85 if "plain" in self.auth_methods or "digest" in self.auth_methods: 86 self.set_iq_get_handler("query","jabber:iq:auth", 87 self.auth_in_stage1) 88 self.set_iq_set_handler("query","jabber:iq:auth", 89 self.auth_in_stage2) 90 elif self.registration_callback: 91 iq = Iq(stanza_type = "get") 92 iq.set_content(Register()) 93 self.set_response_handlers(iq, self.registration_form_received, self.registration_error) 94 self.send(iq) 95 return 96 ClientStream._post_connect(self)
97
98 - def _post_auth(self):
99 """Unregister legacy authentication handlers after successfull 100 authentication.""" 101 ClientStream._post_auth(self) 102 if not self.initiator: 103 self.unset_iq_get_handler("query","jabber:iq:auth") 104 self.unset_iq_set_handler("query","jabber:iq:auth")
105
106 - def _try_auth(self):
107 """Try to authenticate using the first one of allowed authentication 108 methods left. 109 110 [client only]""" 111 if self.authenticated: 112 self.__logger.debug("try_auth: already authenticated") 113 return 114 self.__logger.debug("trying auth: %r" % (self._auth_methods_left,)) 115 if not self._auth_methods_left: 116 raise LegacyAuthenticationError,"No allowed authentication methods available" 117 method=self._auth_methods_left[0] 118 if method.startswith("sasl:"): 119 return ClientStream._try_auth(self) 120 elif method not in ("plain","digest"): 121 self._auth_methods_left.pop(0) 122 self.__logger.debug("Skipping unknown auth method: %s" % method) 123 return self._try_auth() 124 elif self.available_auth_methods is not None: 125 if method in self.available_auth_methods: 126 self._auth_methods_left.pop(0) 127 self.auth_method_used=method 128 if method=="digest": 129 self._digest_auth_stage2(self.auth_stanza) 130 else: 131 self._plain_auth_stage2(self.auth_stanza) 132 self.auth_stanza=None 133 return 134 else: 135 self.__logger.debug("Skipping unavailable auth method: %s" % method) 136 else: 137 self._auth_stage1()
138
139 - def auth_in_stage1(self,stanza):
140 """Handle the first stage (<iq type='get'/>) of legacy ("plain" or 141 "digest") authentication. 142 143 [server only]""" 144 self.lock.acquire() 145 try: 146 if "plain" not in self.auth_methods and "digest" not in self.auth_methods: 147 iq=stanza.make_error_response("not-allowed") 148 self.send(iq) 149 return 150 151 iq=stanza.make_result_response() 152 q=iq.new_query("jabber:iq:auth") 153 q.newChild(None,"username",None) 154 q.newChild(None,"resource",None) 155 if "plain" in self.auth_methods: 156 q.newChild(None,"password",None) 157 if "digest" in self.auth_methods: 158 q.newChild(None,"digest",None) 159 self.send(iq) 160 iq.free() 161 finally: 162 self.lock.release()
163
164 - def auth_in_stage2(self,stanza):
165 """Handle the second stage (<iq type='set'/>) of legacy ("plain" or 166 "digest") authentication. 167 168 [server only]""" 169 self.lock.acquire() 170 try: 171 if "plain" not in self.auth_methods and "digest" not in self.auth_methods: 172 iq=stanza.make_error_response("not-allowed") 173 self.send(iq) 174 return 175 176 username=stanza.xpath_eval("a:query/a:username",{"a":"jabber:iq:auth"}) 177 if username: 178 username=from_utf8(username[0].getContent()) 179 resource=stanza.xpath_eval("a:query/a:resource",{"a":"jabber:iq:auth"}) 180 if resource: 181 resource=from_utf8(resource[0].getContent()) 182 if not username or not resource: 183 self.__logger.debug("No username or resource found in auth request") 184 iq=stanza.make_error_response("bad-request") 185 self.send(iq) 186 return 187 188 if stanza.xpath_eval("a:query/a:password",{"a":"jabber:iq:auth"}): 189 if "plain" not in self.auth_methods: 190 iq=stanza.make_error_response("not-allowed") 191 self.send(iq) 192 return 193 else: 194 return self._plain_auth_in_stage2(username,resource,stanza) 195 if stanza.xpath_eval("a:query/a:digest",{"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._digest_auth_in_stage2(username,resource,stanza) 202 finally: 203 self.lock.release()
204
205 - def _auth_stage1(self):
206 """Do the first stage (<iq type='get'/>) of legacy ("plain" or 207 "digest") authentication. 208 209 [client only]""" 210 iq=Iq(stanza_type="get") 211 q=iq.new_query("jabber:iq:auth") 212 q.newTextChild(None,"username",to_utf8(self.my_jid.node)) 213 q.newTextChild(None,"resource",to_utf8(self.my_jid.resource)) 214 self.send(iq) 215 self.set_response_handlers(iq,self.auth_stage2,self.auth_error, 216 self.auth_timeout,timeout=60) 217 iq.free()
218
219 - def auth_timeout(self):
220 """Handle legacy authentication timeout. 221 222 [client only]""" 223 self.lock.acquire() 224 try: 225 self.__logger.debug("Timeout while waiting for jabber:iq:auth result") 226 if self._auth_methods_left: 227 self._auth_methods_left.pop(0) 228 finally: 229 self.lock.release()
230
231 - def auth_error(self,stanza):
232 """Handle legacy authentication error. 233 234 [client only]""" 235 self.lock.acquire() 236 try: 237 err=stanza.get_error() 238 ae=err.xpath_eval("e:*",{"e":"jabber:iq:auth:error"}) 239 if ae: 240 ae=ae[0].name 241 else: 242 ae=err.get_condition().name 243 raise LegacyAuthenticationError,("Authentication error condition: %s" 244 % (ae,)) 245 finally: 246 self.lock.release()
247
248 - def auth_stage2(self,stanza):
249 """Handle the first stage authentication response (result of the <iq 250 type="get"/>). 251 252 [client only]""" 253 self.lock.acquire() 254 try: 255 self.__logger.debug("Procesing auth response...") 256 self.available_auth_methods=[] 257 if (stanza.xpath_eval("a:query/a:digest",{"a":"jabber:iq:auth"}) and self.stream_id): 258 self.available_auth_methods.append("digest") 259 if (stanza.xpath_eval("a:query/a:password",{"a":"jabber:iq:auth"})): 260 self.available_auth_methods.append("plain") 261 self.auth_stanza=stanza.copy() 262 self._try_auth() 263 finally: 264 self.lock.release()
265
266 - def _plain_auth_stage2(self, _unused):
267 """Do the second stage (<iq type='set'/>) of legacy "plain" 268 authentication. 269 270 [client only]""" 271 iq=Iq(stanza_type="set") 272 q=iq.new_query("jabber:iq:auth") 273 q.newTextChild(None,"username",to_utf8(self.my_jid.node)) 274 q.newTextChild(None,"resource",to_utf8(self.my_jid.resource)) 275 q.newTextChild(None,"password",to_utf8(self.password)) 276 self.send(iq) 277 self.set_response_handlers(iq,self.auth_finish,self.auth_error) 278 iq.free()
279
280 - def _plain_auth_in_stage2(self, username, _unused, stanza):
281 """Handle the second stage (<iq type='set'/>) of legacy "plain" 282 authentication. 283 284 [server only]""" 285 password=stanza.xpath_eval("a:query/a:password",{"a":"jabber:iq:auth"}) 286 if password: 287 password=from_utf8(password[0].getContent()) 288 if not password: 289 self.__logger.debug("No password found in plain auth request") 290 iq=stanza.make_error_response("bad-request") 291 self.send(iq) 292 return 293 294 if self.check_password(username,password): 295 iq=stanza.make_result_response() 296 self.send(iq) 297 self.peer_authenticated=True 298 self.auth_method_used="plain" 299 self.state_change("authorized",self.peer) 300 self._post_auth() 301 else: 302 self.__logger.debug("Plain auth failed") 303 iq=stanza.make_error_response("bad-request") 304 e=iq.get_error() 305 e.add_custom_condition('jabber:iq:auth:error',"user-unauthorized") 306 self.send(iq)
307
308 - def _digest_auth_stage2(self, _unused):
309 """Do the second stage (<iq type='set'/>) of legacy "digest" 310 authentication. 311 312 [client only]""" 313 iq=Iq(stanza_type="set") 314 q=iq.new_query("jabber:iq:auth") 315 q.newTextChild(None,"username",to_utf8(self.my_jid.node)) 316 q.newTextChild(None,"resource",to_utf8(self.my_jid.resource)) 317 318 digest = hashlib.sha1(to_utf8(self.stream_id)+to_utf8(self.password)).hexdigest() 319 320 q.newTextChild(None,"digest",digest) 321 self.send(iq) 322 self.set_response_handlers(iq,self.auth_finish,self.auth_error) 323 iq.free()
324
325 - def _digest_auth_in_stage2(self, username, _unused, stanza):
326 """Handle the second stage (<iq type='set'/>) of legacy "digest" 327 authentication. 328 329 [server only]""" 330 digest=stanza.xpath_eval("a:query/a:digest",{"a":"jabber:iq:auth"}) 331 if digest: 332 digest=digest[0].getContent() 333 if not digest: 334 self.__logger.debug("No digest found in digest auth request") 335 iq=stanza.make_error_response("bad-request") 336 self.send(iq) 337 return 338 339 password,pwformat=self.get_password(username) 340 if not password or pwformat!="plain": 341 iq=stanza.make_error_response("bad-request") 342 e=iq.get_error() 343 e.add_custom_condition('jabber:iq:auth:error',"user-unauthorized") 344 self.send(iq) 345 return 346 347 mydigest = hashlib.sha1(to_utf8(self.stream_id)+to_utf8(password)).hexdigest() 348 349 if mydigest==digest: 350 iq=stanza.make_result_response() 351 self.send(iq) 352 self.peer_authenticated=True 353 self.auth_method_used="digest" 354 self.state_change("authorized",self.peer) 355 self._post_auth() 356 else: 357 self.__logger.debug("Digest auth failed: %r != %r" % (digest,mydigest)) 358 iq=stanza.make_error_response("bad-request") 359 e=iq.get_error() 360 e.add_custom_condition('jabber:iq:auth:error',"user-unauthorized") 361 self.send(iq)
362
363 - def auth_finish(self, _unused):
364 """Handle success of the legacy authentication.""" 365 self.lock.acquire() 366 try: 367 self.__logger.debug("Authenticated") 368 self.authenticated=True 369 self.state_change("authorized",self.my_jid) 370 self._post_auth() 371 finally: 372 self.lock.release()
373
374 - def registration_error(self, stanza):
375 """Handle in-band registration error. 376 377 [client only] 378 379 :Parameters: 380 - `stanza`: the error stanza received or `None` on timeout. 381 :Types: 382 - `stanza`: `pyxmpp.stanza.Stanza`""" 383 self.lock.acquire() 384 try: 385 err=stanza.get_error() 386 ae=err.xpath_eval("e:*",{"e":"jabber:iq:auth:error"}) 387 if ae: 388 ae=ae[0].name 389 else: 390 ae=err.get_condition().name 391 raise RegistrationError,("Authentication error condition: %s" % (ae,)) 392 finally: 393 self.lock.release()
394
395 - def registration_form_received(self, stanza):
396 """Handle registration form received. 397 398 [client only] 399 400 Call self.registration_callback with the registration form received 401 as the argument. Use the value returned by the callback will be a 402 filled-in form. 403 404 :Parameters: 405 - `stanza`: the stanza received. 406 :Types: 407 - `stanza`: `pyxmpp.iq.Iq`""" 408 self.lock.acquire() 409 try: 410 self.__register = Register(stanza.get_query()) 411 self.registration_callback(stanza, self.__register.get_form()) 412 finally: 413 self.lock.release()
414
415 - def submit_registration_form(self, form):
416 """Submit a registration form. 417 418 [client only] 419 420 :Parameters: 421 - `form`: the filled-in form. When form is `None` or its type is 422 "cancel" the registration is to be canceled. 423 424 :Types: 425 - `form`: `pyxmpp.jabber.dataforms.Form`""" 426 self.lock.acquire() 427 try: 428 if form and form.type!="cancel": 429 self.registration_form = form 430 iq = Iq(stanza_type = "set") 431 iq.set_content(self.__register.submit_form(form)) 432 self.set_response_handlers(iq, self.registration_success, self.registration_error) 433 self.send(iq) 434 else: 435 self.__register = None 436 finally: 437 self.lock.release()
438
439 - def registration_success(self, stanza):
440 """Handle registration success. 441 442 [client only] 443 444 Clean up registration stuff, change state to "registered" and initialize 445 authentication. 446 447 :Parameters: 448 - `stanza`: the stanza received. 449 :Types: 450 - `stanza`: `pyxmpp.iq.Iq`""" 451 _unused = stanza 452 self.lock.acquire() 453 try: 454 self.state_change("registered", self.registration_form) 455 if ('FORM_TYPE' in self.registration_form 456 and self.registration_form['FORM_TYPE'].value == 'jabber:iq:register'): 457 if 'username' in self.registration_form: 458 self.my_jid = JID(self.registration_form['username'].value, 459 self.my_jid.domain, self.my_jid.resource) 460 if 'password' in self.registration_form: 461 self.password = self.registration_form['password'].value 462 self.registration_callback = None 463 self._post_connect() 464 finally: 465 self.lock.release()
466 467 # vi: sts=4 et sw=4 468