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