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