diff options
Diffstat (limited to 'nbxmpp/client.py')
-rw-r--r-- | nbxmpp/client.py | 639 |
1 files changed, 0 insertions, 639 deletions
diff --git a/nbxmpp/client.py b/nbxmpp/client.py deleted file mode 100644 index d01af4e..0000000 --- a/nbxmpp/client.py +++ /dev/null @@ -1,639 +0,0 @@ -## client.py -## -## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov -## modified by Dimitur Kirov <dkirov@gmail.com> -## -## This program is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published by -## the Free Software Foundation; either version 2, or (at your option) -## any later version. -## -## This program is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. - -""" -Client class establishes connection to XMPP Server and handles authentication -""" - -import socket -import logging - -from . import transports, dispatcher, roster, protocol, bosh -from .protocol import NS_TLS -from .protocol import JID -from .auth import SASL -from .bind import NonBlockingBind -from .smacks import Smacks -from .const import Realm -from .const import Event - - -log = logging.getLogger('nbxmpp.client') - - -class NonBlockingClient: - """ - Client class is XMPP connection mountpoint. Objects for authentication, - network communication, roster, xml parsing ... are plugged to client object. - Client implements the abstract behavior - mostly negotiation and callbacks - handling, whereas underlying modules take care of feature-specific logic - """ - - def __init__(self, domain, idlequeue, caller=None, lang='en'): - """ - Caches connection data - - :param domain: domain - for to: attribute (from account info) - :param idlequeue: processing idlequeue - :param caller: calling object - it has to implement methods - _event_dispatcher which is called from dispatcher instance - :param lang: the preferred stream language - """ - self.Namespace = protocol.NS_CLIENT - self.defaultNamespace = self.Namespace - self.lang = lang - - self.idlequeue = idlequeue - self.disconnect_handlers = [] - - self.Server = domain - self.xmpp_hostname = None # FQDN hostname to connect to - - # caller is who initiated this client, it is in needed to register - # the EventDispatcher - self._caller = caller - self._owner = self - self._registered_name = None # our full jid, set after successful bind - self.connected = '' - self.ip_addresses = [] - self.socket = None - self.on_connect = None - self.on_proxy_failure = None - self.on_connect_failure = None - self.proxy = None - self.got_features = False - self.got_see_other_host = None - self.stream_started = False - self.disconnecting = False - self.protocol_type = 'XMPP' - self.alpn = False - - # Smacks must retain data over multiple connects/disconnects - Smacks.get_instance().PlugIn(self) - - def set_resume_data(self, data): - ''' - Dict with values we pass to Smacks for session resumption - This is only needed if between reconnects a new NonBlockingClient - instance is created - ''' - self.Smacks.set_resume_data(data) - - def get_resume_data(self): - ''' - returns a dict with values that are necessary to resume a stream - see set_resume_data() - ''' - if not self.sm_enabled or not self.resume_supported: - return {} - return self.Smacks.get_resume_data() - - @property - def sm_enabled(self): - return self.Smacks.enabled - - @property - def resume_supported(self): - return self.Smacks.resume_supported - - def set_bound_jid(self, jid): - if jid is None: - return - jid = JID(jid) - self._registered_name = jid - self.User = jid.getNode() - self.Resource = jid.getResource() - - def get_bound_jid(self): - return self._registered_name - - def get_ssl_connection(self): - if 'NonBlockingTCP' in self.__dict__: - return self.NonBlockingTCP.get_ssl_connection() - - def disconnect(self, message=''): - """ - Called on disconnection - disconnect callback is picked based on state of - the client. - """ - # to avoid recursive calls - if self.ip_addresses: - self._try_next_ip() - return - if self.disconnecting: return - - log.info('Disconnecting NBClient: %s' % message) - - sasl_failed = False - if 'NonBlockingRoster' in self.__dict__: - self.NonBlockingRoster.PlugOut() - if 'SASL' in self.__dict__: - self.SASL.PlugOut() - if 'NonBlockingTCP' in self.__dict__: - self.NonBlockingTCP.PlugOut() - if 'NonBlockingHTTP' in self.__dict__: - self.NonBlockingHTTP.PlugOut() - if 'NonBlockingBOSH' in self.__dict__: - self.NonBlockingBOSH.PlugOut() - # FIXME: we never unplug dispatcher, only on next connect - # See _xmpp_connect_machine and SASLHandler - - connected = self.connected - stream_started = self.stream_started - - self.connected = '' - self.stream_started = False - - self.disconnecting = True - - log.debug('Client disconnected..') - # Don't call any callback when it's a SASL failure. - # SASL handler is already called - if connected == '' and not sasl_failed: - # if we're disconnecting before connection to XMPP sever is opened, - # we don't call disconnect handlers but on_connect_failure callback - if self.proxy: - # with proxy, we have different failure callback - log.debug('calling on_proxy_failure cb') - self.on_proxy_failure(reason=message) - else: - log.debug('calling on_connect_failure cb') - self.on_connect_failure() - elif not sasl_failed: - # we are connected to XMPP server - if not stream_started: - # if error occur before XML stream was opened, e.g. no response on - # init request, we call the on_connect_failure callback because - # proper connection is not established yet and it's not a proxy - # issue - log.debug('calling on_connect_failure cb') - self._caller.streamError = message - self.on_connect_failure() - else: - # with open connection, we are calling the disconnect handlers - for i in reversed(self.disconnect_handlers): - log.debug('Calling disconnect handler %s' % i) - i() - self.disconnecting = False - - def connect(self, on_connect, on_connect_failure, hostname=None, port=5222, - on_proxy_failure=None, on_stream_error_cb=None, proxy=None, - secure_tuple=('tls', None, None, None, None, False)): - """ - Open XMPP connection (open XML streams in both directions) - - :param on_connect: called after stream is successfully opened - :param on_connect_failure: called when error occurs during connection - :param hostname: hostname of XMPP server from SRV request - :param port: port number of XMPP server - :param on_proxy_failure: called if error occurs during TCP connection - to proxy server or during proxy connecting process - :param on_stream_error_cb: called if error occurs - :param proxy: dictionary with proxy data. It should contain at least - values for keys 'host' and 'port' - connection details for proxy - serve and optionally keys 'user' and 'pass' as proxy credentials - :param secure_tuple: tuple of (desired connection type, cacerts, - mycerts, tls_version, cipher_list, alpn) - connection type can be 'ssl' - TLS established after TCP connection, - 'tls' - TLS established after negotiation with starttls, or - 'plain'. - cacerts, mycerts, tls_version, cipher_list, alpn - see tls.NonBlockingTLS constructor for more details - """ - self.on_connect = on_connect - self.on_connect_failure=on_connect_failure - self.on_proxy_failure = on_proxy_failure - self.on_stream_error_cb = on_stream_error_cb - self.desired_security, self.cacerts, self.mycerts, self.tls_version, \ - self.cipher_list = secure_tuple[:5] - if len(secure_tuple) == 6: - # ALPN support was added in version 0.6.3 - self.alpn = secure_tuple[5] - self.Connection = None - self.Port = port - self.proxy = proxy - - if hostname: - self.xmpp_hostname = hostname - else: - self.xmpp_hostname = self.Server - - # We only check for SSL here as for TLS we will first have to start a - # PLAIN connection and negotiate TLS afterwards. - # establish_tls will instruct transport to start secure connection - # directly - establish_tls = self.desired_security == 'ssl' - certs = (self.cacerts, self.mycerts) - - proxy_dict = {} - tcp_host = self.xmpp_hostname - tcp_port = self.Port - - if proxy: - # with proxies, client connects to proxy instead of directly to - # XMPP server ((hostname, port)) - # tcp_host is hostname of machine used for socket connection - # (DNS request will be done for proxy or BOSH CM hostname) - tcp_host, tcp_port, proxy_user, proxy_pass = \ - transports.get_proxy_data_from_dict(proxy) - - if proxy['type'] == 'bosh': - # Setup BOSH transport - self.socket = bosh.NonBlockingBOSH.get_instance( - on_disconnect=self.disconnect, - raise_event=self.raise_event, - idlequeue=self.idlequeue, - estabilish_tls=establish_tls, - certs=certs, - tls_version = self.tls_version, - cipher_list = self.cipher_list, - proxy_creds=(proxy_user, proxy_pass), - xmpp_server=(self.xmpp_hostname, self.Port), - domain=self.Server, - bosh_dict=proxy) - self.protocol_type = 'BOSH' - self.wait_for_restart_response = \ - proxy['bosh_wait_for_restart_response'] - else: - # http proxy - proxy_dict['type'] = proxy['type'] - proxy_dict['xmpp_server'] = (self.xmpp_hostname, self.Port) - proxy_dict['credentials'] = (proxy_user, proxy_pass) - - if not proxy or proxy['type'] != 'bosh': - # Setup ordinary TCP transport - self.socket = transports.NonBlockingTCP.get_instance( - on_disconnect=self.disconnect, - raise_event=self.raise_event, - idlequeue=self.idlequeue, - estabilish_tls=establish_tls, - certs=certs, - tls_version = self.tls_version, - cipher_list = self.cipher_list, - alpn=self.alpn, - proxy_dict=proxy_dict) - - # plug transport into client as self.Connection - self.socket.PlugIn(self) - - self._resolve_hostname( - hostname=tcp_host, - port=tcp_port, - on_success=self._try_next_ip) - - def _resolve_hostname(self, hostname, port, on_success): - """ - Wrapper for getaddinfo call - - FIXME: getaddinfo blocks - """ - try: - self.ip_addresses = socket.getaddrinfo(hostname, port, - socket.AF_UNSPEC, socket.SOCK_STREAM) - except socket.gaierror as exc: - self.disconnect(message='Lookup failure for %s:%s, hostname: %s - %s' % - (self.Server, self.Port, hostname, str(exc))) - except socket.error as exc: - errnum, errstr = exc.errno, exc.strerror - self.disconnect(message='General socket error for %s:%s, hostname: ' - '%s - %s' % (self.Server, self.Port, hostname, errstr)) - else: - on_success() - - def _try_next_ip(self, err_message=None): - """ - Iterate over IP addresses tries to connect to it - """ - if err_message: - log.debug('While looping over DNS A records: %s' % err_message) - if not self.ip_addresses: - msg = 'Run out of hosts for name %s:%s.' % (self.Server, self.Port) - msg += ' Error for last IP: %s' % err_message - self.disconnect(msg) - else: - self.current_ip = self.ip_addresses.pop(0) - self.socket.connect( - conn_5tuple=self.current_ip, - on_connect=lambda: self._xmpp_connect(), - on_connect_failure=self._try_next_ip) - - def incoming_stream_version(self): - """ - Get version of xml stream - """ - if 'version' in self.Dispatcher.Stream._document_attrs: - return self.Dispatcher.Stream._document_attrs['version'] - else: - return None - - def _xmpp_connect(self, socket_type=None): - """ - Start XMPP connecting process - open the XML stream. Is called after TCP - connection is established or after switch to TLS when successfully - negotiated with <starttls>. - """ - # socket_type contains info which transport connection was established - if not socket_type: - if self.Connection.ssl_lib: - # When ssl_lib is set we connected via SSL - socket_type = 'ssl' - else: - # PLAIN is default - socket_type = 'plain' - self.connected = socket_type - self._xmpp_connect_machine() - - def _xmpp_connect_machine(self, mode=None, data=None): - """ - Finite automaton taking care of stream opening and features tag handling. - Calls _on_stream_start when stream is started, and disconnect() on - failure. - """ - log.info('-------------xmpp_connect_machine() >> mode: %s, data: %s...' % - (mode, str(data)[:20])) - - def on_next_receive(mode): - """ - Set desired on_receive callback on transport based on the state of - connect_machine. - """ - log.info('setting %s on next receive' % mode) - if mode is None: - self.onreceive(None) # switch to Dispatcher.ProcessNonBlocking - else: - self.onreceive(lambda _data:self._xmpp_connect_machine(mode, _data)) - - if not mode: - # starting state - if 'Dispatcher' in self.__dict__: - self.Dispatcher.PlugOut() - self.got_features = False - dispatcher.Dispatcher.get_instance().PlugIn(self) - on_next_receive('RECEIVE_DOCUMENT_ATTRIBUTES') - - elif mode == 'FAILURE': - self.disconnect('During XMPP connect: %s' % data) - - elif mode == 'RECEIVE_DOCUMENT_ATTRIBUTES': - if data: - self.Dispatcher.ProcessNonBlocking(data) - self.ip_addresses = [] - if not hasattr(self, 'Dispatcher') or \ - self.Dispatcher.Stream._document_attrs is None: - self._xmpp_connect_machine(mode='FAILURE', - data='Error on stream open') - return - - # if terminating stanza was received after init request then client gets - # disconnected from bosh transport plugin and we have to end the stream - # negotiating process straight away. - # fixes #4657 - if not self.connected: return - - if self.incoming_stream_version() == '1.0': - if not self.got_features: - on_next_receive('RECEIVE_STREAM_FEATURES') - else: - log.info('got STREAM FEATURES in first recv') - self._xmpp_connect_machine(mode='STREAM_STARTED') - else: - log.info('incoming stream version less than 1.0') - self._xmpp_connect_machine(mode='STREAM_STARTED') - - elif mode == 'RECEIVE_STREAM_FEATURES': - if data: - # sometimes <features> are received together with document - # attributes and sometimes on next receive... - self.Dispatcher.ProcessNonBlocking(data) - if self.got_see_other_host: - log.info('got see-other-host') - self.onreceive(None) - self.on_stream_error_cb(self, self.got_see_other_host) - elif not self.got_features: - self._xmpp_connect_machine(mode='FAILURE', - data='Missing <features> in 1.0 stream') - else: - log.info('got STREAM FEATURES in second recv') - self._xmpp_connect_machine(mode='STREAM_STARTED') - - elif mode == 'STREAM_STARTED': - self._on_stream_start() - - def _on_stream_start(self): - """ - Called after XMPP stream is opened. TLS negotiation may follow if - supported and desired. - """ - self.stream_started = True - if not hasattr(self, 'onreceive'): - # we may already have been disconnected - return - self.onreceive(None) - - if self.connected == 'plain': - if self.desired_security == 'plain': - # if we want and have plain connection, we're done now - self._on_connect() - else: - # try to negotiate TLS - if self.incoming_stream_version() != '1.0': - # if stream version is less than 1.0, we can't do more - log.info('While connecting with type = "tls": stream version ' + - 'is less than 1.0') - self._on_connect() - return - if self.Dispatcher.Stream.features.getTag('starttls'): - # Server advertises TLS support, start negotiation - self.stream_started = False - log.info('TLS supported by remote server. Requesting TLS start.') - self._tls_negotiation_handler() - else: - log.info('While connecting with type = "tls": TLS unsupported ' + - 'by remote server') - self._on_connect() - - elif self.connected in ['ssl', 'tls']: - self._on_connect() - else: - assert False, 'Stream opened for unsupported connection' - - def _tls_negotiation_handler(self, con=None, tag=None): - """ - Take care of TLS negotiation with <starttls> - """ - log.info('-------------tls_negotiaton_handler() >> tag: %s' % tag) - if not con and not tag: - # starting state when we send the <starttls> - self.RegisterHandlerOnce('proceed', self._tls_negotiation_handler, - xmlns=NS_TLS) - self.RegisterHandlerOnce('failure', self._tls_negotiation_handler, - xmlns=NS_TLS) - self.send('<starttls xmlns="%s"/>' % NS_TLS) - else: - # we got <proceed> or <failure> - if tag.getNamespace() != NS_TLS: - self.disconnect('Unknown namespace: %s' % tag.getNamespace()) - return - tagname = tag.getName() - if tagname == 'failure': - self.disconnect('TLS <failure> received: %s' % tag) - return - log.info('Got starttls proceed response. Switching to TLS/SSL...') - # following call wouldn't work for BOSH transport but it doesn't matter - # because <starttls> negotiation with BOSH is forbidden - self.Connection.tls_init( - on_succ = lambda: self._xmpp_connect(socket_type='tls'), - on_fail = lambda: self.disconnect('error while establishing TLS')) - - def _on_connect(self): - """ - Preceed call of on_connect callback - """ - self.onreceive(None) - self.on_connect(self, self.connected) - - def raise_event(self, event_type, data): - """ - Raise event to connection instance. DATA_SENT and DATA_RECIVED events - are used in XML console to show XMPP traffic - """ - e_t = event_type - if type(event_type) != str: - e_t = event_type.encode('utf-8') - log.info('raising event from transport: :::::%s::::\n_____________\n%s\n_____________\n' % (e_t, data)) - if hasattr(self, 'Dispatcher'): - self.Dispatcher.Event('', event_type, data) - -############################################################################### -### follows code for authentication, resource bind, session and roster download -############################################################################### - - def auth(self, user, get_password=None, resource='', auth_mechs=None): - """ - Authenticate connnection and bind resource. If resource is not provided - random one or library name used - - :param user: XMPP username - :param get_password: Callback that must return the password for the - chosen mechanism - :param resource: resource that shall be used for auth/connecting - :param auth_mechs: Set of valid authentification mechanisms. If None all - authentification mechanisms will be allowed. - See the auth module for possible values - """ - if 'SASL' in self.__dict__: - log.error('Auth not possible while another auth is in progress') - return - - self._User = user - self._Resource = resource - - self.onreceive(None) - SASL.get_instance(self._User, - auth_mechs, - get_password, - self._on_sasl_finished).PlugIn(self) - - def _on_sasl_finished(self, success, reason, text): - if success: - self.SASL.PlugOut() - if self.protocol_type == 'BOSH': - self.Dispatcher.after_SASL = True - self.Dispatcher.StreamInit() - - self.connected += '+sasl' - self.Dispatcher.Event(Realm.CONNECTING, - Event.AUTH_SUCCESSFUL) - self._owner.Smacks.register_handlers() - else: - self.Dispatcher.Event(Realm.CONNECTING, - Event.AUTH_FAILED, - (reason, text)) - - def bind(self): - # Check if we can resume - if self.Smacks.resume_supported: - self.Smacks.resume_request() - else: - # If we cant resume we bind and enable sm afterwards - NonBlockingBind.get_instance().PlugIn(self) - - def initRoster(self, version='', request=True): - """ - Plug in the roster - """ - if 'NonBlockingRoster' not in self.__dict__: - return roster.NonBlockingRoster.get_instance( - version=version).PlugIn(self, request=request) - - def getRoster(self, on_ready=None, force=False): - """ - Return the Roster instance, previously plugging it in and requesting - roster from server if needed - """ - if 'NonBlockingRoster' in self.__dict__: - return self.NonBlockingRoster.getRoster(on_ready, force) - return None - - def sendPresence(self, jid=None, typ=None, requestRoster=0): - """ - Send some specific presence state. Can also request roster from server if - according agrument is set - """ - if requestRoster: - # FIXME: used somewhere? - roster.NonBlockingRoster.get_instance().PlugIn(self) - self.send(dispatcher.Presence(to=jid, typ=typ)) - -############################################################################### -### following methods are moved from blocking client class of xmpppy -############################################################################### - - def RegisterDisconnectHandler(self, handler): - """ - Register handler that will be called on disconnect - """ - self.disconnect_handlers.append(handler) - - def UnregisterDisconnectHandler(self, handler): - """ - Unregister handler that is called on disconnect - """ - self.disconnect_handlers.remove(handler) - - def DisconnectHandler(self): - """ - Default disconnect handler. Just raises an IOError. If you choosed to use - this class in your production client, override this method or at least - unregister it. - """ - raise IOError('Disconnected from server.') - - def get_connect_type(self): - """ - Return connection state. F.e.: None / 'tls' / 'plain+non_sasl' - """ - return self.connected - - def get_peerhost(self): - """ - Gets the ip address of the account, from which is made connection to the - server (e.g. IP and port of socket) - - We will create listening socket on the same ip - """ - # FIXME: tuple (ip, port) is expected (and checked for) but port num is - # useless - return self.socket.peerhost |