Package common :: Package xmpp :: Module client_nb
[hide private]
[frames] | no frames]

Source Code for Module common.xmpp.client_nb

  1  ##   client_nb.py 
  2  ##         based on client.py, changes backported up to revision 1.60 
  3  ## 
  4  ##   Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov 
  5  ##         modified by Dimitur Kirov <dkirov@gmail.com> 
  6  ## 
  7  ##   This program is free software; you can redistribute it and/or modify 
  8  ##   it under the terms of the GNU General Public License as published by 
  9  ##   the Free Software Foundation; either version 2, or (at your option) 
 10  ##   any later version. 
 11  ## 
 12  ##   This program is distributed in the hope that it will be useful, 
 13  ##   but WITHOUT ANY WARRANTY; without even the implied warranty of 
 14  ##   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 15  ##   GNU General Public License for more details. 
 16   
 17  # $Id: client.py,v 1.52 2006/01/02 19:40:55 normanr Exp $ 
 18   
 19  """ 
 20  Client class establishs connection to XMPP Server and handles authentication 
 21  """ 
 22   
 23  import socket 
 24  import transports_nb, dispatcher_nb, auth_nb, roster_nb, protocol, bosh 
 25  from protocol import NS_TLS 
 26   
 27  import logging 
 28  log = logging.getLogger('gajim.c.x.client_nb') 
 29   
 30   
31 -class NonBlockingClient:
32 """ 33 Client class is XMPP connection mountpoint. Objects for authentication, 34 network communication, roster, xml parsing ... are plugged to client object. 35 Client implements the abstract behavior - mostly negotioation and callbacks 36 handling, whereas underlying modules take care of feature-specific logic 37 """ 38
39 - def __init__(self, domain, idlequeue, caller=None):
40 """ 41 Caches connection data 42 43 :param domain: domain - for to: attribute (from account info) 44 :param idlequeue: processing idlequeue 45 :param caller: calling object - it has to implement methods 46 _event_dispatcher which is called from dispatcher instance 47 """ 48 self.Namespace = protocol.NS_CLIENT 49 self.defaultNamespace = self.Namespace 50 51 self.idlequeue = idlequeue 52 self.disconnect_handlers = [] 53 54 self.Server = domain 55 self.xmpp_hostname = None # FQDN hostname to connect to 56 57 # caller is who initiated this client, it is in needed to register 58 # the EventDispatcher 59 self._caller = caller 60 self._owner = self 61 self._registered_name = None # our full jid, set after successful auth 62 self.connected = '' 63 self.socket = None 64 self.on_connect = None 65 self.on_proxy_failure = None 66 self.on_connect_failure = None 67 self.proxy = None 68 self.got_features = False 69 self.stream_started = False 70 self.disconnecting = False 71 self.protocol_type = 'XMPP'
72
73 - def disconnect(self, message=''):
74 """ 75 Called on disconnection - disconnect callback is picked based on state of 76 the client. 77 """ 78 # to avoid recursive calls 79 if self.disconnecting: return 80 81 log.info('Disconnecting NBClient: %s' % message) 82 83 if 'NonBlockingRoster' in self.__dict__: 84 self.NonBlockingRoster.PlugOut() 85 if 'NonBlockingBind' in self.__dict__: 86 self.NonBlockingBind.PlugOut() 87 if 'NonBlockingNonSASL' in self.__dict__: 88 self.NonBlockingNonSASL.PlugOut() 89 if 'SASL' in self.__dict__: 90 self.SASL.PlugOut() 91 if 'NonBlockingTCP' in self.__dict__: 92 self.NonBlockingTCP.PlugOut() 93 if 'NonBlockingHTTP' in self.__dict__: 94 self.NonBlockingHTTP.PlugOut() 95 if 'NonBlockingBOSH' in self.__dict__: 96 self.NonBlockingBOSH.PlugOut() 97 # FIXME: we never unplug dispatcher, only on next connect 98 # See _xmpp_connect_machine and SASLHandler 99 100 connected = self.connected 101 stream_started = self.stream_started 102 103 self.connected = '' 104 self.stream_started = False 105 106 self.disconnecting = True 107 108 log.debug('Client disconnected..') 109 if connected == '': 110 # if we're disconnecting before connection to XMPP sever is opened, 111 # we don't call disconnect handlers but on_connect_failure callback 112 if self.proxy: 113 # with proxy, we have different failure callback 114 log.debug('calling on_proxy_failure cb') 115 self.on_proxy_failure(reason=message) 116 else: 117 log.debug('calling on_connect_failure cb') 118 self.on_connect_failure() 119 else: 120 # we are connected to XMPP server 121 if not stream_started: 122 # if error occur before XML stream was opened, e.g. no response on 123 # init request, we call the on_connect_failure callback because 124 # proper connection is not established yet and it's not a proxy 125 # issue 126 log.debug('calling on_connect_failure cb') 127 self._caller.streamError = message 128 self.on_connect_failure() 129 else: 130 # with open connection, we are calling the disconnect handlers 131 for i in reversed(self.disconnect_handlers): 132 log.debug('Calling disconnect handler %s' % i) 133 i() 134 self.disconnecting = False
135
136 - def connect(self, on_connect, on_connect_failure, hostname=None, port=5222, 137 on_proxy_failure=None, proxy=None, secure_tuple=('plain', None, 138 None)):
139 """ 140 Open XMPP connection (open XML streams in both directions) 141 142 :param on_connect: called after stream is successfully opened 143 :param on_connect_failure: called when error occures during connection 144 :param hostname: hostname of XMPP server from SRV request 145 :param port: port number of XMPP server 146 :param on_proxy_failure: called if error occurres during TCP connection to 147 proxy server or during proxy connecting process 148 :param proxy: dictionary with proxy data. It should contain at least 149 values for keys 'host' and 'port' - connection details for proxy serve 150 and optionally keys 'user' and 'pass' as proxy credentials 151 :param secure_tuple: tuple of (desired connection type, cacerts, mycerts) 152 connection type can be 'ssl' - TLS established after TCP connection, 153 'tls' - TLS established after negotiation with starttls, or 'plain'. 154 cacerts, mycerts - see tls_nb.NonBlockingTLS constructor for more 155 details 156 """ 157 self.on_connect = on_connect 158 self.on_connect_failure=on_connect_failure 159 self.on_proxy_failure = on_proxy_failure 160 self.desired_security, self.cacerts, self.mycerts = secure_tuple 161 self.Connection = None 162 self.Port = port 163 self.proxy = proxy 164 165 if hostname: 166 self.xmpp_hostname = hostname 167 else: 168 self.xmpp_hostname = self.Server 169 170 # We only check for SSL here as for TLS we will first have to start a 171 # PLAIN connection and negotiate TLS afterwards. 172 # establish_tls will instruct transport to start secure connection 173 # directly 174 establish_tls = self.desired_security == 'ssl' 175 certs = (self.cacerts, self.mycerts) 176 177 proxy_dict = {} 178 tcp_host = self.xmpp_hostname 179 tcp_port = self.Port 180 181 if proxy: 182 # with proxies, client connects to proxy instead of directly to 183 # XMPP server ((hostname, port)) 184 # tcp_host is hostname of machine used for socket connection 185 # (DNS request will be done for proxy or BOSH CM hostname) 186 tcp_host, tcp_port, proxy_user, proxy_pass = \ 187 transports_nb.get_proxy_data_from_dict(proxy) 188 189 if proxy['type'] == 'bosh': 190 # Setup BOSH transport 191 self.socket = bosh.NonBlockingBOSH.get_instance( 192 on_disconnect=self.disconnect, 193 raise_event=self.raise_event, 194 idlequeue=self.idlequeue, 195 estabilish_tls=establish_tls, 196 certs=certs, 197 proxy_creds=(proxy_user, proxy_pass), 198 xmpp_server=(self.xmpp_hostname, self.Port), 199 domain=self.Server, 200 bosh_dict=proxy) 201 self.protocol_type = 'BOSH' 202 self.wait_for_restart_response = \ 203 proxy['bosh_wait_for_restart_response'] 204 else: 205 # http proxy 206 proxy_dict['type'] = proxy['type'] 207 proxy_dict['xmpp_server'] = (self.xmpp_hostname, self.Port) 208 proxy_dict['credentials'] = (proxy_user, proxy_pass) 209 210 if not proxy or proxy['type'] != 'bosh': 211 # Setup ordinary TCP transport 212 self.socket = transports_nb.NonBlockingTCP.get_instance( 213 on_disconnect=self.disconnect, 214 raise_event=self.raise_event, 215 idlequeue=self.idlequeue, 216 estabilish_tls=establish_tls, 217 certs=certs, 218 proxy_dict=proxy_dict) 219 220 # plug transport into client as self.Connection 221 self.socket.PlugIn(self) 222 223 self._resolve_hostname( 224 hostname=tcp_host, 225 port=tcp_port, 226 on_success=self._try_next_ip)
227
228 - def _resolve_hostname(self, hostname, port, on_success):
229 """ 230 Wrapper for getaddinfo call 231 232 FIXME: getaddinfo blocks 233 """ 234 try: 235 self.ip_addresses = socket.getaddrinfo(hostname, port, 236 socket.AF_UNSPEC, socket.SOCK_STREAM) 237 except socket.gaierror, (errnum, errstr): 238 self.disconnect(message='Lookup failure for %s:%s, hostname: %s - %s' % 239 (self.Server, self.Port, hostname, errstr)) 240 else: 241 on_success()
242
243 - def _try_next_ip(self, err_message=None):
244 """ 245 Iterate over IP addresses tries to connect to it 246 """ 247 if err_message: 248 log.debug('While looping over DNS A records: %s' % err_message) 249 if self.ip_addresses == []: 250 msg = 'Run out of hosts for name %s:%s.' % (self.Server, self.Port) 251 msg = msg + ' Error for last IP: %s' % err_message 252 self.disconnect(msg) 253 else: 254 self.current_ip = self.ip_addresses.pop(0) 255 self.socket.connect( 256 conn_5tuple=self.current_ip, 257 on_connect=lambda: self._xmpp_connect(), 258 on_connect_failure=self._try_next_ip)
259
260 - def incoming_stream_version(self):
261 """ 262 Get version of xml stream 263 """ 264 if 'version' in self.Dispatcher.Stream._document_attrs: 265 return self.Dispatcher.Stream._document_attrs['version'] 266 else: 267 return None
268
269 - def _xmpp_connect(self, socket_type=None):
270 """ 271 Start XMPP connecting process - open the XML stream. Is called after TCP 272 connection is established or after switch to TLS when successfully 273 negotiated with <starttls>. 274 """ 275 # socket_type contains info which transport connection was established 276 if not socket_type: 277 if self.Connection.ssl_lib: 278 # When ssl_lib is set we connected via SSL 279 socket_type = 'ssl' 280 else: 281 # PLAIN is default 282 socket_type = 'plain' 283 self.connected = socket_type 284 self._xmpp_connect_machine()
285
286 - def _xmpp_connect_machine(self, mode=None, data=None):
287 """ 288 Finite automaton taking care of stream opening and features tag handling. 289 Calls _on_stream_start when stream is started, and disconnect() on 290 failure. 291 """ 292 log.info('-------------xmpp_connect_machine() >> mode: %s, data: %s...' % 293 (mode, str(data)[:20])) 294 295 def on_next_receive(mode): 296 """ 297 Set desired on_receive callback on transport based on the state of 298 connect_machine. 299 """ 300 log.info('setting %s on next receive' % mode) 301 if mode is None: 302 self.onreceive(None) # switch to Dispatcher.ProcessNonBlocking 303 else: 304 self.onreceive(lambda _data:self._xmpp_connect_machine(mode, _data))
305 306 if not mode: 307 # starting state 308 if self.__dict__.has_key('Dispatcher'): 309 self.Dispatcher.PlugOut() 310 self.got_features = False 311 dispatcher_nb.Dispatcher.get_instance().PlugIn(self) 312 on_next_receive('RECEIVE_DOCUMENT_ATTRIBUTES') 313 314 elif mode == 'FAILURE': 315 self.disconnect('During XMPP connect: %s' % data) 316 317 elif mode == 'RECEIVE_DOCUMENT_ATTRIBUTES': 318 if data: 319 self.Dispatcher.ProcessNonBlocking(data) 320 if not hasattr(self, 'Dispatcher') or \ 321 self.Dispatcher.Stream._document_attrs is None: 322 self._xmpp_connect_machine( 323 mode='FAILURE', 324 data='Error on stream open') 325 return 326 327 # if terminating stanza was received after init request then client gets 328 # disconnected from bosh transport plugin and we have to end the stream 329 # negotiating process straight away. 330 # fixes #4657 331 if not self.connected: return 332 333 if self.incoming_stream_version() == '1.0': 334 if not self.got_features: 335 on_next_receive('RECEIVE_STREAM_FEATURES') 336 else: 337 log.info('got STREAM FEATURES in first recv') 338 self._xmpp_connect_machine(mode='STREAM_STARTED') 339 else: 340 log.info('incoming stream version less than 1.0') 341 self._xmpp_connect_machine(mode='STREAM_STARTED') 342 343 elif mode == 'RECEIVE_STREAM_FEATURES': 344 if data: 345 # sometimes <features> are received together with document 346 # attributes and sometimes on next receive... 347 self.Dispatcher.ProcessNonBlocking(data) 348 if not self.got_features: 349 self._xmpp_connect_machine( 350 mode='FAILURE', 351 data='Missing <features> in 1.0 stream') 352 else: 353 log.info('got STREAM FEATURES in second recv') 354 self._xmpp_connect_machine(mode='STREAM_STARTED') 355 356 elif mode == 'STREAM_STARTED': 357 self._on_stream_start()
358
359 - def _on_stream_start(self):
360 """ 361 Called after XMPP stream is opened. TLS negotiation may follow if 362 supported and desired. 363 """ 364 self.stream_started = True 365 if not hasattr(self, 'onreceive'): 366 # we may already have been disconnected 367 return 368 self.onreceive(None) 369 370 if self.connected == 'plain': 371 if self.desired_security == 'plain': 372 # if we want and have plain connection, we're done now 373 self._on_connect() 374 else: 375 # try to negotiate TLS 376 if self.incoming_stream_version() != '1.0': 377 # if stream version is less than 1.0, we can't do more 378 log.info('While connecting with type = "tls": stream version ' + 379 'is less than 1.0') 380 self._on_connect() 381 return 382 if self.Dispatcher.Stream.features.getTag('starttls'): 383 # Server advertises TLS support, start negotiation 384 self.stream_started = False 385 log.info('TLS supported by remote server. Requesting TLS start.') 386 self._tls_negotiation_handler() 387 else: 388 log.info('While connecting with type = "tls": TLS unsupported ' + 389 'by remote server') 390 self._on_connect() 391 392 elif self.connected in ['ssl', 'tls']: 393 self._on_connect() 394 else: 395 assert False, 'Stream opened for unsupported connection'
396
397 - def _tls_negotiation_handler(self, con=None, tag=None):
398 """ 399 Take care of TLS negotioation with <starttls> 400 """ 401 log.info('-------------tls_negotiaton_handler() >> tag: %s' % tag) 402 if not con and not tag: 403 # starting state when we send the <starttls> 404 self.RegisterHandlerOnce('proceed', self._tls_negotiation_handler, 405 xmlns=NS_TLS) 406 self.RegisterHandlerOnce('failure', self._tls_negotiation_handler, 407 xmlns=NS_TLS) 408 self.send('<starttls xmlns="%s"/>' % NS_TLS) 409 else: 410 # we got <proceed> or <failure> 411 if tag.getNamespace() != NS_TLS: 412 self.disconnect('Unknown namespace: %s' % tag.getNamespace()) 413 return 414 tagname = tag.getName() 415 if tagname == 'failure': 416 self.disconnect('TLS <failure> received: %s' % tag) 417 return 418 log.info('Got starttls proceed response. Switching to TLS/SSL...') 419 # following call wouldn't work for BOSH transport but it doesn't matter 420 # because <starttls> negotiation with BOSH is forbidden 421 self.Connection.tls_init( 422 on_succ = lambda: self._xmpp_connect(socket_type='tls'), 423 on_fail = lambda: self.disconnect('error while etabilishing TLS'))
424
425 - def _on_connect(self):
426 """ 427 Preceed call of on_connect callback 428 """ 429 self.onreceive(None) 430 self.on_connect(self, self.connected)
431
432 - def raise_event(self, event_type, data):
433 """ 434 Raise event to connection instance. DATA_SENT and DATA_RECIVED events 435 are used in XML console to show XMPP traffic 436 """ 437 log.info('raising event from transport: :::::%s::::\n_____________\n%s\n_____________\n' % (event_type, data)) 438 if hasattr(self, 'Dispatcher'): 439 self.Dispatcher.Event('', event_type, data)
440 441 ############################################################################### 442 ### follows code for authentication, resource bind, session and roster download 443 ############################################################################### 444
445 - def auth(self, user, password, resource='', sasl=True, on_auth=None):
446 """ 447 Authenticate connnection and bind resource. If resource is not provided 448 random one or library name used 449 450 :param user: XMPP username 451 :param password: XMPP password 452 :param resource: resource that shall be used for auth/connecting 453 :param sasl: Boolean indicating if SASL shall be used. (default: True) 454 :param on_auth: Callback, called after auth. On auth failure, argument 455 is None. 456 """ 457 self._User, self._Password = user, password 458 self._Resource, self._sasl = resource, sasl 459 self.on_auth = on_auth 460 self._on_doc_attrs() 461 return
462
463 - def _on_old_auth(self, res):
464 """ 465 Callback used by NON-SASL auth. On auth failure, res is None 466 """ 467 if res: 468 self.connected += '+old_auth' 469 self.on_auth(self, 'old_auth') 470 else: 471 self.on_auth(self, None)
472
473 - def _on_sasl_auth(self, res):
474 """ 475 Used internally. On auth failure, res is None 476 """ 477 self.onreceive(None) 478 if res: 479 self.connected += '+sasl' 480 self.on_auth(self, 'sasl') 481 else: 482 self.on_auth(self, None)
483
484 - def _on_doc_attrs(self):
485 """ 486 Plug authentication objects and start auth 487 """ 488 if self._sasl: 489 auth_nb.SASL.get_instance(self._User, self._Password, 490 self._on_start_sasl).PlugIn(self) 491 if not self._sasl or self.SASL.startsasl == 'not-supported': 492 if not self._Resource: 493 self._Resource = 'xmpppy' 494 auth_nb.NonBlockingNonSASL.get_instance(self._User, self._Password, 495 self._Resource, self._on_old_auth).PlugIn(self) 496 return 497 self.SASL.auth() 498 return True
499
500 - def _on_start_sasl(self, data=None):
501 """ 502 Callback used by SASL, called on each auth step 503 """ 504 if data: 505 self.Dispatcher.ProcessNonBlocking(data) 506 if not 'SASL' in self.__dict__: 507 # SASL is pluged out, possible disconnect 508 return 509 if self.SASL.startsasl == 'in-process': 510 return 511 self.onreceive(None) 512 if self.SASL.startsasl == 'failure': 513 # wrong user/pass, stop auth 514 if 'SASL' in self.__dict__: 515 self.SASL.PlugOut() 516 self.connected = None # FIXME: is this intended? We use ''elsewhere 517 self._on_sasl_auth(None) 518 elif self.SASL.startsasl == 'success': 519 auth_nb.NonBlockingBind.get_instance().PlugIn(self) 520 self.onreceive(self._on_auth_bind) 521 return True
522
523 - def _on_auth_bind(self, data):
524 # FIXME: Why use this callback and not bind directly? 525 if data: 526 self.Dispatcher.ProcessNonBlocking(data) 527 if self.NonBlockingBind.bound is None: 528 return 529 self.NonBlockingBind.NonBlockingBind(self._Resource, self._on_sasl_auth) 530 return True
531
532 - def initRoster(self, version=''):
533 """ 534 Plug in the roster 535 """ 536 if not self.__dict__.has_key('NonBlockingRoster'): 537 return roster_nb.NonBlockingRoster.get_instance(version=version).PlugIn(self)
538
539 - def getRoster(self, on_ready=None, force=False):
540 """ 541 Return the Roster instance, previously plugging it in and requesting 542 roster from server if needed 543 """ 544 if self.__dict__.has_key('NonBlockingRoster'): 545 return self.NonBlockingRoster.getRoster(on_ready, force) 546 return None
547
548 - def sendPresence(self, jid=None, typ=None, requestRoster=0):
549 """ 550 Send some specific presence state. Can also request roster from server if 551 according agrument is set 552 """ 553 if requestRoster: 554 # FIXME: used somewhere? 555 roster_nb.NonBlockingRoster.get_instance().PlugIn(self) 556 self.send(dispatcher_nb.Presence(to=jid, typ=typ))
557 558 ############################################################################### 559 ### following methods are moved from blocking client class of xmpppy 560 ############################################################################### 561
562 - def RegisterDisconnectHandler(self, handler):
563 """ 564 Register handler that will be called on disconnect 565 """ 566 self.disconnect_handlers.append(handler)
567
568 - def UnregisterDisconnectHandler(self, handler):
569 """ 570 Unregister handler that is called on disconnect 571 """ 572 self.disconnect_handlers.remove(handler)
573
574 - def DisconnectHandler(self):
575 """ 576 Default disconnect handler. Just raises an IOError. If you choosed to use 577 this class in your production client, override this method or at least 578 unregister it. 579 """ 580 raise IOError('Disconnected from server.')
581
582 - def get_connect_type(self):
583 """ 584 Return connection state. F.e.: None / 'tls' / 'plain+non_sasl' 585 """ 586 return self.connected
587
588 - def get_peerhost(self):
589 """ 590 Gets the ip address of the account, from which is made connection to the 591 server (e.g. IP and port of gajim's socket) 592 593 We will create listening socket on the same ip 594 """ 595 # FIXME: tuple (ip, port) is expected (and checked for) but port num is 596 # useless 597 return self.socket.peerhost
598