Package pyxmpp :: Module clientstream
[hide private]

Source Code for Module pyxmpp.clientstream

  1  # 
  2  # (C) Copyright 2003-2006 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 652 2006-08-27 19:41:15Z 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 self.authenticated: 173 self.__logger.debug("try_auth: already authenticated") 174 return 175 self.__logger.debug("trying auth: %r", (self._auth_methods_left,)) 176 if not self._auth_methods_left: 177 raise StreamAuthenticationError,"No allowed authentication methods available" 178 method=self._auth_methods_left[0] 179 if method.startswith("sasl:"): 180 if self.version: 181 self._auth_methods_left.pop(0) 182 try: 183 self._sasl_authenticate(self.my_jid.node, None, 184 mechanism=method[5:].upper()) 185 except (SASLMechanismNotAvailable,SASLNotAvailable): 186 self.__logger.debug("Skipping unavailable auth method: %s", (method,) ) 187 return self._try_auth() 188 else: 189 self._auth_methods_left.pop(0) 190 self.__logger.debug("Skipping auth method %s as legacy protocol is in use", 191 (method,) ) 192 return self._try_auth() 193 else: 194 self._auth_methods_left.pop(0) 195 self.__logger.debug("Skipping unknown auth method: %s", method) 196 return self._try_auth()
197
198 - def _get_stream_features(self):
199 """Include resource binding feature in the stream features list. 200 201 [server only]""" 202 features=Stream._get_stream_features(self) 203 if self.peer_authenticated: 204 bind=features.newChild(None,"bind",None) 205 ns=bind.newNs(BIND_NS,None) 206 bind.setNs(ns) 207 self.set_iq_set_handler("bind",BIND_NS,self.do_bind) 208 return features
209
210 - def do_bind(self,stanza):
211 """Do the resource binding requested by a client connected. 212 213 [server only] 214 215 :Parameters: 216 - `stanza`: resource binding request stanza. 217 :Types: 218 - `stanza`: `pyxmpp.Iq`""" 219 fr=stanza.get_from() 220 if fr and fr!=self.peer: 221 r=stanza.make_error_response("forbidden") 222 self.send(r) 223 r.free() 224 return 225 resource_n=stanza.xpath_eval("bind:bind/bind:resource",{"bind":BIND_NS}) 226 if resource_n: 227 resource=resource_n[0].getContent() 228 else: 229 resource="auto" 230 if not resource: 231 r=stanza.make_error_response("bad-request") 232 else: 233 self.unset_iq_set_handler("bind",BIND_NS) 234 r=stanza.make_result_response() 235 self.peer.set_resource(resource) 236 q=r.new_query(BIND_NS,"bind") 237 q.newTextChild(None,"jid",to_utf8(self.peer.as_unicode())) 238 self.state_change("authorized",self.peer) 239 r.set_to(None) 240 self.send(r) 241 r.free()
242
243 - def get_password(self, username, realm=None, acceptable_formats=("plain",)):
244 """Get a user password for the SASL authentication. 245 246 :Parameters: 247 - `username`: username used for authentication. 248 - `realm`: realm used for authentication. 249 - `acceptable_formats`: acceptable password encoding formats requested. 250 :Types: 251 - `username`: `unicode` 252 - `realm`: `unicode` 253 - `acceptable_formats`: `list` of `str` 254 255 :return: The password and the format name ('plain'). 256 :returntype: (`unicode`,`str`)""" 257 _unused = realm 258 if self.initiator and self.my_jid.node==username and "plain" in acceptable_formats: 259 return self.password,"plain" 260 else: 261 return None,None
262
263 - def get_realms(self):
264 """Get realms available for client authentication. 265 266 [server only] 267 268 :return: list of realms. 269 :returntype: `list` of `unicode`""" 270 return [self.my_jid.domain]
271
272 - def choose_realm(self,realm_list):
273 """Choose authentication realm from the list provided by the server. 274 275 [client only] 276 277 Use domain of the own JID if no realm list was provided or the domain is on the list 278 or the first realm on the list otherwise. 279 280 :Parameters: 281 - `realm_list`: realm list provided by the server. 282 :Types: 283 - `realm_list`: `list` of `unicode` 284 285 :return: the realm chosen. 286 :returntype: `unicode`""" 287 if not realm_list: 288 return self.my_jid.domain 289 if self.my_jid.domain in realm_list: 290 return self.my_jid.domain 291 return realm_list[0]
292
293 - def check_authzid(self,authzid,extra_info=None):
294 """Check authorization id provided by the client. 295 296 [server only] 297 298 :Parameters: 299 - `authzid`: authorization id provided. 300 - `extra_info`: additional information about the user 301 from the authentication backend. This mapping will 302 usually contain at least 'username' item. 303 :Types: 304 - `authzid`: unicode 305 - `extra_info`: mapping 306 307 :return: `True` if user is authorized to use that `authzid`. 308 :returntype: `bool`""" 309 if not extra_info: 310 extra_info={} 311 if not authzid: 312 return 1 313 if not self.initiator: 314 jid=JID(authzid) 315 if not extra_info.has_key("username"): 316 ret=0 317 elif jid.node!=extra_info["username"]: 318 ret=0 319 elif jid.domain!=self.my_jid.domain: 320 ret=0 321 elif not jid.resource: 322 ret=0 323 else: 324 ret=1 325 else: 326 ret=0 327 return ret
328
329 - def get_serv_type(self):
330 """Get the server name for SASL authentication. 331 332 :return: 'xmpp'.""" 333 return "xmpp"
334
335 - def get_serv_name(self):
336 """Get the service name for SASL authentication. 337 338 :return: domain of the own JID.""" 339 return self.my_jid.domain
340
341 - def get_serv_host(self):
342 """Get the service host name for SASL authentication. 343 344 :return: domain of the own JID.""" 345 # FIXME: that should be the hostname choosen from SRV records found. 346 return self.my_jid.domain
347
348 - def fix_out_stanza(self,stanza):
349 """Fix outgoing stanza. 350 351 On a client clear the sender JID. On a server set the sender 352 address to the own JID if the address is not set yet.""" 353 if self.initiator: 354 stanza.set_from(None) 355 else: 356 if not stanza.get_from(): 357 stanza.set_from(self.my_jid)
358
359 - def fix_in_stanza(self,stanza):
360 """Fix an incoming stanza. 361 362 Ona server replace the sender address with authorized client JID.""" 363 if self.initiator: 364 Stream.fix_in_stanza(self,stanza) 365 else: 366 stanza.set_from(self.peer)
367 368 # vi: sts=4 et sw=4 369