Package pyxmpp :: Module clientstream
[hide private]

Source Code for Module pyxmpp.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  # pylint: disable-msg=W0221 
 18   
 19  """Client stream handling. 
 20   
 21  Normative reference: 
 22    - `RFC 3920 <http://www.ietf.org/rfc/rfc3920.txt>`__ 
 23  """ 
 24   
 25  __revision__="$Id: clientstream.py 714 2010-04-05 10:20:10Z jajcus $" 
 26  __docformat__="restructuredtext en" 
 27   
 28  import logging 
 29   
 30  from pyxmpp.stream import Stream 
 31  from pyxmpp.streambase import BIND_NS 
 32  from pyxmpp.streamsasl import SASLNotAvailable,SASLMechanismNotAvailable 
 33  from pyxmpp.jid import JID 
 34  from pyxmpp.utils import to_utf8 
 35  from pyxmpp.exceptions import StreamError,StreamAuthenticationError,FatalStreamError 
 36  from pyxmpp.exceptions import ClientStreamError, FatalClientStreamError 
 37   
38 -class ClientStream(Stream):
39 """Handles XMPP-IM client connection stream. 40 41 Both client and server side of the connection is supported. This class handles 42 client SASL authentication, authorisation and resource binding. 43 44 This class is not ready for handling of legacy Jabber servers, as it doesn't 45 provide legacy authentication. 46 47 :Ivariables: 48 - `my_jid`: requested local JID. Please notice that this may differ from 49 `me`, which is actual authorized JID after the resource binding. 50 - `server`: server to use. 51 - `port`: port number to use. 52 - `password`: user's password. 53 - `auth_methods`: allowed authentication methods. 54 :Types: 55 - `my_jid`: `pyxmpp.JID` 56 - `server`: `str` 57 - `port`: `int` 58 - `password`: `str` 59 - `auth_methods`: `list` of `str` 60 """
61 - def __init__(self, jid, password=None, server=None, port=None, 62 auth_methods = ("sasl:DIGEST-MD5",), 63 tls_settings = None, keepalive = 0, owner = None):
64 """Initialize the ClientStream object. 65 66 :Parameters: 67 - `jid`: local JID. 68 - `password`: user's password. 69 - `server`: server to use. If not given then address will be derived form the JID. 70 - `port`: port number to use. If not given then address will be derived form the JID. 71 - `auth_methods`: sallowed authentication methods. SASL authentication mechanisms 72 in the list should be prefixed with "sasl:" string. 73 - `tls_settings`: settings for StartTLS -- `TLSSettings` instance. 74 - `keepalive`: keepalive output interval. 0 to disable. 75 - `owner`: `Client`, `Component` or similar object "owning" this stream. 76 :Types: 77 - `jid`: `pyxmpp.JID` 78 - `password`: `unicode` 79 - `server`: `unicode` 80 - `port`: `int` 81 - `auth_methods`: sequence of `str` 82 - `tls_settings`: `pyxmpp.TLSSettings` 83 - `keepalive`: `int` 84 """ 85 sasl_mechanisms=[] 86 for m in auth_methods: 87 if not m.startswith("sasl:"): 88 continue 89 m=m[5:].upper() 90 sasl_mechanisms.append(m) 91 Stream.__init__(self, "jabber:client", 92 sasl_mechanisms = sasl_mechanisms, 93 tls_settings = tls_settings, 94 keepalive = keepalive, 95 owner = owner) 96 self.server=server 97 self.port=port 98 self.password=password 99 self.auth_methods=auth_methods 100 self.my_jid=jid 101 self.me = None 102 self._auth_methods_left = None 103 self.__logger=logging.getLogger("pyxmpp.ClientStream")
104
105 - def _reset(self):
106 """Reset `ClientStream` object state, making the object ready to handle 107 new connections.""" 108 Stream._reset(self) 109 self._auth_methods_left=[]
110
111 - def connect(self,server=None,port=None):
112 """Establish a client connection to a server. 113 114 [client only] 115 116 :Parameters: 117 - `server`: name or address of the server to use. Not recommended -- proper value 118 should be derived automatically from the JID. 119 - `port`: port number of the server to use. Not recommended -- 120 proper value should be derived automatically from the JID. 121 122 :Types: 123 - `server`: `unicode` 124 - `port`: `int`""" 125 self.lock.acquire() 126 try: 127 self._connect(server,port) 128 finally: 129 self.lock.release()
130
131 - def _connect(self,server=None,port=None):
132 """Same as `ClientStream.connect` but assume `self.lock` is acquired.""" 133 if not self.my_jid.node or not self.my_jid.resource: 134 raise ClientStreamError,"Client JID must have username and resource" 135 if not server: 136 server=self.server 137 if not port: 138 port=self.port 139 if server: 140 self.__logger.debug("server: %r", (server,)) 141 service=None 142 else: 143 service="xmpp-client" 144 if port is None: 145 port=5222 146 if server is None: 147 server=self.my_jid.domain 148 self.me=self.my_jid 149 Stream._connect(self,server,port,service,self.my_jid.domain)
150
151 - def accept(self,sock):
152 """Accept an incoming client connection. 153 154 [server only] 155 156 :Parameters: 157 - `sock`: a listening socket.""" 158 Stream.accept(self,sock,self.my_jid)
159
160 - def _post_connect(self):
161 """Initialize authentication when the connection is established 162 and we are the initiator.""" 163 if self.initiator: 164 self._auth_methods_left=list(self.auth_methods) 165 self._try_auth()
166
167 - def _try_auth(self):
168 """Try to authenticate using the first one of allowed authentication 169 methods left. 170 171 [client only]""" 172 if not self.doc_out: 173 self.__logger.debug("try_auth: disconnecting already?") 174 return 175 if self.authenticated: 176 self.__logger.debug("try_auth: already authenticated") 177 return 178 self.__logger.debug("trying auth: %r", (self._auth_methods_left,)) 179 if not self._auth_methods_left: 180 raise StreamAuthenticationError,"No allowed authentication methods available" 181 method=self._auth_methods_left[0] 182 if method.startswith("sasl:"): 183 if self.version: 184 self._auth_methods_left.pop(0) 185 try: 186 self._sasl_authenticate(self.my_jid.node, None, 187 mechanism=method[5:].upper()) 188 except (SASLMechanismNotAvailable,SASLNotAvailable): 189 self.__logger.debug("Skipping unavailable auth method: %s", (method,) ) 190 return self._try_auth() 191 else: 192 self._auth_methods_left.pop(0) 193 self.__logger.debug("Skipping auth method %s as legacy protocol is in use", 194 (method,) ) 195 return self._try_auth() 196 else: 197 self._auth_methods_left.pop(0) 198 self.__logger.debug("Skipping unknown auth method: %s", method) 199 return self._try_auth()
200
201 - def _get_stream_features(self):
202 """Include resource binding feature in the stream features list. 203 204 [server only]""" 205 features=Stream._get_stream_features(self) 206 if self.peer_authenticated: 207 bind=features.newChild(None,"bind",None) 208 ns=bind.newNs(BIND_NS,None) 209 bind.setNs(ns) 210 self.set_iq_set_handler("bind",BIND_NS,self.do_bind) 211 return features
212
213 - def do_bind(self,stanza):
214 """Do the resource binding requested by a client connected. 215 216 [server only] 217 218 :Parameters: 219 - `stanza`: resource binding request stanza. 220 :Types: 221 - `stanza`: `pyxmpp.Iq`""" 222 fr=stanza.get_from() 223 if fr and fr!=self.peer: 224 r=stanza.make_error_response("forbidden") 225 self.send(r) 226 r.free() 227 return 228 resource_n=stanza.xpath_eval("bind:bind/bind:resource",{"bind":BIND_NS}) 229 if resource_n: 230 resource=resource_n[0].getContent() 231 else: 232 resource="auto" 233 if not resource: 234 r=stanza.make_error_response("bad-request") 235 else: 236 self.unset_iq_set_handler("bind",BIND_NS) 237 r=stanza.make_result_response() 238 self.peer.set_resource(resource) 239 q=r.new_query(BIND_NS,"bind") 240 q.newTextChild(None,"jid",to_utf8(self.peer.as_unicode())) 241 self.state_change("authorized",self.peer) 242 r.set_to(None) 243 self.send(r) 244 r.free()
245
246 - def get_password(self, username, realm=None, acceptable_formats=("plain",)):
247 """Get a user password for the SASL authentication. 248 249 :Parameters: 250 - `username`: username used for authentication. 251 - `realm`: realm used for authentication. 252 - `acceptable_formats`: acceptable password encoding formats requested. 253 :Types: 254 - `username`: `unicode` 255 - `realm`: `unicode` 256 - `acceptable_formats`: `list` of `str` 257 258 :return: The password and the format name ('plain'). 259 :returntype: (`unicode`,`str`)""" 260 _unused = realm 261 if self.initiator and self.my_jid.node==username and "plain" in acceptable_formats: 262 return self.password,"plain" 263 else: 264 return None,None
265
266 - def get_realms(self):
267 """Get realms available for client authentication. 268 269 [server only] 270 271 :return: list of realms. 272 :returntype: `list` of `unicode`""" 273 return [self.my_jid.domain]
274
275 - def choose_realm(self,realm_list):
276 """Choose authentication realm from the list provided by the server. 277 278 [client only] 279 280 Use domain of the own JID if no realm list was provided or the domain is on the list 281 or the first realm on the list otherwise. 282 283 :Parameters: 284 - `realm_list`: realm list provided by the server. 285 :Types: 286 - `realm_list`: `list` of `unicode` 287 288 :return: the realm chosen. 289 :returntype: `unicode`""" 290 if not realm_list: 291 return self.my_jid.domain 292 if self.my_jid.domain in realm_list: 293 return self.my_jid.domain 294 return realm_list[0]
295
296 - def check_authzid(self,authzid,extra_info=None):
297 """Check authorization id provided by the client. 298 299 [server only] 300 301 :Parameters: 302 - `authzid`: authorization id provided. 303 - `extra_info`: additional information about the user 304 from the authentication backend. This mapping will 305 usually contain at least 'username' item. 306 :Types: 307 - `authzid`: unicode 308 - `extra_info`: mapping 309 310 :return: `True` if user is authorized to use that `authzid`. 311 :returntype: `bool`""" 312 if not extra_info: 313 extra_info={} 314 if not authzid: 315 return 1 316 if not self.initiator: 317 jid=JID(authzid) 318 if not extra_info.has_key("username"): 319 ret=0 320 elif jid.node!=extra_info["username"]: 321 ret=0 322 elif jid.domain!=self.my_jid.domain: 323 ret=0 324 elif not jid.resource: 325 ret=0 326 else: 327 ret=1 328 else: 329 ret=0 330 return ret
331
332 - def get_serv_type(self):
333 """Get the server name for SASL authentication. 334 335 :return: 'xmpp'.""" 336 return "xmpp"
337
338 - def get_serv_name(self):
339 """Get the service name for SASL authentication. 340 341 :return: domain of the own JID.""" 342 return self.my_jid.domain
343
344 - def get_serv_host(self):
345 """Get the service host name for SASL authentication. 346 347 :return: domain of the own JID.""" 348 # FIXME: that should be the hostname choosen from SRV records found. 349 return self.my_jid.domain
350
351 - def fix_out_stanza(self,stanza):
352 """Fix outgoing stanza. 353 354 On a client clear the sender JID. On a server set the sender 355 address to the own JID if the address is not set yet.""" 356 if self.initiator: 357 stanza.set_from(None) 358 else: 359 if not stanza.get_from(): 360 stanza.set_from(self.my_jid)
361
362 - def fix_in_stanza(self,stanza):
363 """Fix an incoming stanza. 364 365 Ona server replace the sender address with authorized client JID.""" 366 if self.initiator: 367 Stream.fix_in_stanza(self,stanza) 368 else: 369 stanza.set_from(self.peer)
370 371 # vi: sts=4 et sw=4 372