Package pyxmpp :: Module streamsasl
[hide private]

Source Code for Module pyxmpp.streamsasl

  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  # pylint: disable-msg=W0201 
 18   
 19  """SASL support XMPP streams. 
 20   
 21  Normative reference: 
 22    - `RFC 3920 <http://www.ietf.org/rfc/rfc3920.txt>`__ 
 23  """ 
 24   
 25  __docformat__="restructuredtext en" 
 26   
 27  import base64 
 28  import logging 
 29   
 30  from pyxmpp.jid import JID 
 31  from pyxmpp import sasl 
 32  from pyxmpp.exceptions import StreamAuthenticationError, SASLNotAvailable, SASLMechanismNotAvailable, SASLAuthenticationFailed 
 33   
 34  SASL_NS="urn:ietf:params:xml:ns:xmpp-sasl" 
 35   
36 -class StreamSASLMixIn(sasl.PasswordManager):
37 """SASL authentication mix-in class for XMPP stream."""
38 - def __init__(self,sasl_mechanisms=()):
39 """Initialize Stream object 40 41 :Parameters: 42 - `sasl_mechanisms`: sequence of SASL mechanisms allowed for 43 authentication. Currently "PLAIN", "DIGEST-MD5" and "GSSAPI" are supported. 44 """ 45 sasl.PasswordManager.__init__(self) 46 if sasl_mechanisms: 47 self.sasl_mechanisms=sasl_mechanisms 48 else: 49 self.sasl_mechanisms=[] 50 self.__logger=logging.getLogger("pyxmpp.StreamSASLMixIn")
51
52 - def _reset_sasl(self):
53 """Reset `StreamSASLMixIn` object state making it ready to handle new 54 connections.""" 55 self.peer_sasl_mechanisms=None 56 self.authenticator=None
57
58 - def _make_stream_sasl_features(self,features):
59 """Add SASL features to the <features/> element of the stream. 60 61 [receving entity only] 62 63 :returns: update <features/> element node.""" 64 if self.sasl_mechanisms and not self.authenticated: 65 ml=features.newChild(None,"mechanisms",None) 66 ns=ml.newNs(SASL_NS,None) 67 ml.setNs(ns) 68 for m in self.sasl_mechanisms: 69 if m in sasl.all_mechanisms: 70 ml.newTextChild(None,"mechanism",m) 71 return features
72
73 - def _handle_sasl_features(self):
74 """Process incoming <stream:features/> element. 75 76 [initiating entity only] 77 78 The received features node is available in `self.features`.""" 79 ctxt = self.doc_in.xpathNewContext() 80 ctxt.setContextNode(self.features) 81 ctxt.xpathRegisterNs("sasl",SASL_NS) 82 try: 83 sasl_mechanisms_n=ctxt.xpathEval("sasl:mechanisms/sasl:mechanism") 84 finally: 85 ctxt.xpathFreeContext() 86 87 if sasl_mechanisms_n: 88 self.__logger.debug("SASL support found") 89 self.peer_sasl_mechanisms=[] 90 for n in sasl_mechanisms_n: 91 self.peer_sasl_mechanisms.append(n.getContent())
92
93 - def _process_node_sasl(self,xmlnode):
94 """Process incoming stream element. Pass it to _process_sasl_node 95 if it is in the SASL namespace. 96 97 :return: `True` when the node was recognized as a SASL element. 98 :returntype: `bool`""" 99 ns_uri=xmlnode.ns().getContent() 100 if ns_uri==SASL_NS: 101 self._process_sasl_node(xmlnode) 102 return True 103 return False
104
105 - def _process_sasl_node(self,xmlnode):
106 """Process stream element in the SASL namespace. 107 108 :Parameters: 109 - `xmlnode`: the XML node received 110 """ 111 if self.initiator: 112 if not self.authenticator: 113 self.__logger.debug("Unexpected SASL response: %r" % (xmlnode.serialize())) 114 ret=False 115 elif xmlnode.name=="challenge": 116 ret=self._process_sasl_challenge(xmlnode.getContent()) 117 elif xmlnode.name=="success": 118 ret=self._process_sasl_success(xmlnode.getContent()) 119 elif xmlnode.name=="failure": 120 ret=self._process_sasl_failure(xmlnode) 121 else: 122 self.__logger.debug("Unexpected SASL node: %r" % (xmlnode.serialize())) 123 ret=False 124 else: 125 if xmlnode.name=="auth": 126 mechanism=xmlnode.prop("mechanism") 127 ret=self._process_sasl_auth(mechanism,xmlnode.getContent()) 128 if xmlnode.name=="response": 129 ret=self._process_sasl_response(xmlnode.getContent()) 130 if xmlnode.name=="abort": 131 ret=self._process_sasl_abort() 132 else: 133 self.__logger.debug("Unexpected SASL node: %r" % (xmlnode.serialize())) 134 ret=False 135 return ret
136
137 - def _process_sasl_auth(self,mechanism,content):
138 """Process incoming <sasl:auth/> element. 139 140 [receiving entity only] 141 142 :Parameters: 143 - `mechanism`: mechanism choosen by the peer. 144 - `content`: optional "initial response" included in the element. 145 """ 146 if self.authenticator: 147 self.__logger.debug("Authentication already started") 148 return False 149 150 self.auth_method_used="sasl:"+mechanism 151 self.authenticator=sasl.server_authenticator_factory(mechanism,self) 152 153 r=self.authenticator.start(base64.decodestring(content)) 154 155 if isinstance(r,sasl.Success): 156 el_name="success" 157 content=r.base64() 158 elif isinstance(r,sasl.Challenge): 159 el_name="challenge" 160 content=r.base64() 161 else: 162 el_name="failure" 163 content=None 164 165 root=self.doc_out.getRootElement() 166 xmlnode=root.newChild(None,el_name,None) 167 ns=xmlnode.newNs(SASL_NS,None) 168 xmlnode.setNs(ns) 169 if content: 170 xmlnode.setContent(content) 171 if isinstance(r,sasl.Failure): 172 xmlnode.newChild(None,r.reason,None) 173 174 self._write_raw(xmlnode.serialize(encoding="UTF-8")) 175 xmlnode.unlinkNode() 176 xmlnode.freeNode() 177 178 if isinstance(r,sasl.Success): 179 if r.authzid: 180 self.peer=JID(r.authzid) 181 else: 182 self.peer=JID(r.username,self.me.domain) 183 self.peer_authenticated=1 184 self.state_change("authenticated",self.peer) 185 self._post_auth() 186 187 if isinstance(r,sasl.Failure): 188 raise SASLAuthenticationFailed,"SASL authentication failed" 189 190 return True
191
192 - def _process_sasl_challenge(self,content):
193 """Process incoming <sasl:challenge/> element. 194 195 [initiating entity only] 196 197 :Parameters: 198 - `content`: the challenge data received (Base64-encoded). 199 """ 200 if not self.authenticator: 201 self.__logger.debug("Unexpected SASL challenge") 202 return False 203 204 r=self.authenticator.challenge(base64.decodestring(content)) 205 if isinstance(r,sasl.Response): 206 el_name="response" 207 content=r.base64() 208 else: 209 el_name="abort" 210 content=None 211 212 root=self.doc_out.getRootElement() 213 xmlnode=root.newChild(None,el_name,None) 214 ns=xmlnode.newNs(SASL_NS,None) 215 xmlnode.setNs(ns) 216 if content: 217 xmlnode.setContent(content) 218 219 self._write_raw(xmlnode.serialize(encoding="UTF-8")) 220 xmlnode.unlinkNode() 221 xmlnode.freeNode() 222 223 if isinstance(r,sasl.Failure): 224 raise SASLAuthenticationFailed,"SASL authentication failed" 225 226 return True
227
228 - def _process_sasl_response(self,content):
229 """Process incoming <sasl:response/> element. 230 231 [receiving entity only] 232 233 :Parameters: 234 - `content`: the response data received (Base64-encoded). 235 """ 236 if not self.authenticator: 237 self.__logger.debug("Unexpected SASL response") 238 return 0 239 240 r=self.authenticator.response(base64.decodestring(content)) 241 if isinstance(r,sasl.Success): 242 el_name="success" 243 content=r.base64() 244 elif isinstance(r,sasl.Challenge): 245 el_name="challenge" 246 content=r.base64() 247 else: 248 el_name="failure" 249 content=None 250 251 root=self.doc_out.getRootElement() 252 xmlnode=root.newChild(None,el_name,None) 253 ns=xmlnode.newNs(SASL_NS,None) 254 xmlnode.setNs(ns) 255 if content: 256 xmlnode.setContent(content) 257 if isinstance(r,sasl.Failure): 258 xmlnode.newChild(None,r.reason,None) 259 260 self._write_raw(xmlnode.serialize(encoding="UTF-8")) 261 xmlnode.unlinkNode() 262 xmlnode.freeNode() 263 264 if isinstance(r,sasl.Success): 265 authzid=r.authzid 266 if authzid: 267 self.peer=JID(r.authzid) 268 else: 269 self.peer=JID(r.username,self.me.domain) 270 self.peer_authenticated=1 271 self._restart_stream() 272 self.state_change("authenticated",self.peer) 273 self._post_auth() 274 275 if isinstance(r,sasl.Failure): 276 raise SASLAuthenticationFailed,"SASL authentication failed" 277 278 return 1
279
280 - def _process_sasl_success(self,content):
281 """Process incoming <sasl:success/> element. 282 283 [initiating entity only] 284 285 :Parameters: 286 - `content`: the "additional data with success" received (Base64-encoded). 287 """ 288 if not self.authenticator: 289 self.__logger.debug("Unexpected SASL response") 290 return False 291 292 r=self.authenticator.finish(base64.decodestring(content)) 293 if isinstance(r,sasl.Success): 294 self.__logger.debug("SASL authentication succeeded") 295 if r.authzid: 296 self.me=JID(r.authzid) 297 else: 298 self.me=self.me 299 self.authenticated=1 300 self._restart_stream() 301 self.state_change("authenticated",self.me) 302 self._post_auth() 303 else: 304 self.__logger.debug("SASL authentication failed") 305 raise SASLAuthenticationFailed,"Additional success data procesing failed" 306 return True
307
308 - def _process_sasl_failure(self,xmlnode):
309 """Process incoming <sasl:failure/> element. 310 311 [initiating entity only] 312 313 :Parameters: 314 - `xmlnode`: the XML node received. 315 """ 316 if not self.authenticator: 317 self.__logger.debug("Unexpected SASL response") 318 return False 319 320 self.__logger.debug("SASL authentication failed: %r" % (xmlnode.serialize(),)) 321 raise SASLAuthenticationFailed,"SASL authentication failed"
322
323 - def _process_sasl_abort(self):
324 """Process incoming <sasl:abort/> element. 325 326 [receiving entity only]""" 327 if not self.authenticator: 328 self.__logger.debug("Unexpected SASL response") 329 return False 330 331 self.authenticator=None 332 self.__logger.debug("SASL authentication aborted") 333 return True
334
335 - def _sasl_authenticate(self,username,authzid,mechanism=None):
336 """Start SASL authentication process. 337 338 [initiating entity only] 339 340 :Parameters: 341 - `username`: user name. 342 - `authzid`: authorization ID. 343 - `mechanism`: SASL mechanism to use.""" 344 if not self.initiator: 345 raise SASLAuthenticationFailed,"Only initiating entity start SASL authentication" 346 while not self.features: 347 self.__logger.debug("Waiting for features") 348 self._read() 349 if not self.peer_sasl_mechanisms: 350 raise SASLNotAvailable,"Peer doesn't support SASL" 351 352 if not mechanism: 353 mechanism=None 354 for m in self.sasl_mechanisms: 355 if m in self.peer_sasl_mechanisms: 356 mechanism=m 357 break 358 if not mechanism: 359 raise SASLMechanismNotAvailable,"Peer doesn't support any of our SASL mechanisms" 360 self.__logger.debug("Our mechanism: %r" % (mechanism,)) 361 else: 362 if mechanism not in self.peer_sasl_mechanisms: 363 raise SASLMechanismNotAvailable,"%s is not available" % (mechanism,) 364 365 self.auth_method_used="sasl:"+mechanism 366 367 self.authenticator=sasl.client_authenticator_factory(mechanism,self) 368 369 initial_response=self.authenticator.start(username,authzid) 370 if not isinstance(initial_response,sasl.Response): 371 raise SASLAuthenticationFailed,"SASL initiation failed" 372 373 root=self.doc_out.getRootElement() 374 xmlnode=root.newChild(None,"auth",None) 375 ns=xmlnode.newNs(SASL_NS,None) 376 xmlnode.setNs(ns) 377 xmlnode.setProp("mechanism",mechanism) 378 if initial_response.data: 379 if initial_response.encode: 380 xmlnode.setContent(initial_response.base64()) 381 else: 382 xmlnode.setContent(initial_response.data) 383 384 self._write_raw(xmlnode.serialize(encoding="UTF-8")) 385 xmlnode.unlinkNode() 386 xmlnode.freeNode()
387 388 # vi: sts=4 et sw=4 389