## client_nb.py ## based on client.py ## ## Copyright (C) 2003-2005 Alexey "Snake" Nezhdanov ## modified by Dimitur Kirov ## ## 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. # $Id: client.py,v 1.52 2006/01/02 19:40:55 normanr Exp $ ''' Provides Client classes implementations as examples of xmpppy structures usage. These classes can be used for simple applications "AS IS" though. ''' import socket import transports_nb, tls_nb, dispatcher_nb, auth_nb, roster_nb, protocol from client import * import logging log = logging.getLogger('gajim.c.x.client_nb') class NBCommonClient: ''' Base for Client and Component classes.''' def __init__(self, domain, idlequeue, caller=None): ''' Caches connection data: :param domain: domain - for to: attribute (from account info) :param idlequeue: processing idlequeue :param port: port of listening XMPP server :param caller: calling object - it has to implement certain methods (necessary?) ''' self.Namespace = protocol.NS_CLIENT self.idlequeue = idlequeue self.defaultNamespace = self.Namespace self.disconnect_handlers = [] self.Server = domain # caller is who initiated this client, it is sed to register the EventDispatcher self._caller = caller self._owner = self self._registered_name = None self.connected = '' self._component=0 self.socket = None self.on_connect = None self.on_proxy_failure = None self.on_connect_failure = None self.proxy = None def on_disconnect(self): ''' Called on disconnection - when connect failure occurs on running connection (after stream is successfully opened). Calls disconnect handlers and cleans things up. ''' self.connected='' log.debug('Client disconnected..') for i in reversed(self.disconnect_handlers): log.debug('Calling disconnect handler %s' % i) i() if self.__dict__.has_key('NonBlockingRoster'): self.NonBlockingRoster.PlugOut() if self.__dict__.has_key('NonBlockingBind'): self.NonBlockingBind.PlugOut() if self.__dict__.has_key('NonBlockingNonSASL'): self.NonBlockingNonSASL.PlugOut() if self.__dict__.has_key('SASL'): self.SASL.PlugOut() if self.__dict__.has_key('NonBlockingTLS'): self.NonBlockingTLS.PlugOut() if self.__dict__.has_key('NBHTTPPROXYsocket'): self.NBHTTPPROXYsocket.PlugOut() if self.__dict__.has_key('NBSOCKS5PROXYsocket'): self.NBSOCKS5PROXYsocket.PlugOut() if self.__dict__.has_key('NonBlockingTcp'): self.NonBlockingTcp.PlugOut() def send(self, stanza, now = False): ''' interface for putting stanzas on wire. Puts ID to stanza if needed and sends it via socket wrapper''' (id, stanza_to_send) = self.Dispatcher.assign_id(stanza) self.Connection.send(stanza_to_send, now = now) return id def connect(self, on_connect, on_connect_failure, hostname=None, port=5222, on_proxy_failure=None, proxy=None, secure=None): ''' Open XMPP connection (open streams in both directions). :param hostname: hostname of XMPP server from SRV request :param port: port number of XMPP server :param on_connect: called after stream is successfully opened :param on_connect_failure: called when error occures during connection :param on_proxy_failure: called if error occurres during TCP connection to proxy server or during connection to the proxy :param proxy: dictionary with proxy data. It should contain at least values for keys 'host' and 'port' - connection details for proxy server and optionally keys 'user' and 'pass' as proxy credentials :param secure: ''' self.Port = port if hostname: xmpp_hostname = hostname else: xmpp_hostname = self.Server self.on_connect = on_connect self.on_connect_failure=on_connect_failure self.on_proxy_failure = on_proxy_failure self._secure = secure self.Connection = None if proxy: # with proxies, client connects to proxy instead of directly to # XMPP server ((hostname, port)) # tcp_server is machine used for socket connection tcp_server=proxy['host'] tcp_port=proxy['port'] self._on_tcp_failure = self.on_proxy_failure if proxy.has_key('type'): if proxy.has_key('user') and proxy.has_key('pass'): proxy_creds=(proxy['user'],proxy['pass']) else: proxy_creds=(None, None) type_ = proxy['type'] if type_ == 'socks5': # SOCKS5 proxy self.socket = transports_nb.NBSOCKS5ProxySocket( on_disconnect=self.on_disconnect, proxy_creds=proxy_creds, xmpp_server=(xmpp_hostname, self.Port)) elif type_ == 'http': # HTTP CONNECT to proxy self.socket = transports_nb.NBHTTPProxySocket( on_disconnect=self.on_disconnect, proxy_creds=proxy_creds, xmpp_server=(xmpp_hostname, self.Port)) elif type_ == 'bosh': # BOSH - XMPP over HTTP tcp_server = transports_nb.urisplit(tcp_server)[1] self.socket = transports_nb.NonBlockingHTTP( on_disconnect=self.on_disconnect, http_uri = proxy['host'], http_port = tcp_port) else: # HTTP CONNECT to proxy from environment variables self.socket = transports_nb.NBHTTPProxySocket( on_disconnect=self.on_disconnect, proxy_creds=(None, None), xmpp_server=(xmpp_hostname, self.Port)) else: self._on_tcp_failure = self._on_connect_failure tcp_server=xmpp_hostname tcp_port=self.Port self.socket = transports_nb.NonBlockingTcp(on_disconnect = self.on_disconnect) self.socket.PlugIn(self) self._resolve_hostname( hostname=tcp_server, port=tcp_port, on_success=self._try_next_ip, on_failure=self._on_tcp_failure) def _resolve_hostname(self, hostname, port, on_success, on_failure): ''' wrapper of getaddinfo call. FIXME: getaddinfo blocks''' try: self.ip_addresses = socket.getaddrinfo(hostname,port, socket.AF_UNSPEC,socket.SOCK_STREAM) except socket.gaierror, (errnum, errstr): on_failure(err_message='Lookup failure for %s:%s - %s %s' % (self.Server, self.Port, errnum, errstr)) else: on_success() def _try_next_ip(self, err_message=None): '''iterates over IP addresses from getaddinfo''' if err_message: log.debug('While looping over DNS A records: %s' % connect) if self.ip_addresses == []: self._on_tcp_failure(err_message='Run out of hosts for name %s:%s' % (self.Server, self.Port)) else: self.current_ip = self.ip_addresses.pop(0) self.socket.connect( conn_5tuple=self.current_ip, on_connect=lambda: self._xmpp_connect(socket_type='tcp'), on_connect_failure=self._try_next_ip) def incoming_stream_version(self): ''' gets version of xml stream''' if self.Dispatcher.Stream._document_attrs.has_key('version'): return self.Dispatcher.Stream._document_attrs['version'] else: return None def _xmpp_connect(self, socket_type): self.connected = socket_type self._xmpp_connect_machine() def _xmpp_connect_machine(self, mode=None, data=None): ''' Finite automaton called after TCP connecting. Takes care of stream opening and features tag handling. Calls _on_stream_start when stream is started, and _on_connect_failure on failure. ''' #FIXME: use RegisterHandlerOnce instead of onreceive log.info('=============xmpp_connect_machine() >> mode: %s, data: %s' % (mode,data)) def on_next_receive(mode): if mode is None: self.onreceive(None) else: self.onreceive(lambda data:self._xmpp_connect_machine(mode, data)) if not mode: dispatcher_nb.Dispatcher().PlugIn(self) on_next_receive('RECEIVE_DOCUMENT_ATTRIBUTES') elif mode == 'FAILURE': self._on_connect_failure(err_message='During XMPP connect: %s' % data) elif mode == 'RECEIVE_DOCUMENT_ATTRIBUTES': if data: self.Dispatcher.ProcessNonBlocking(data) if not hasattr(self, 'Dispatcher') or \ self.Dispatcher.Stream._document_attrs is None: self._xmpp_connect_machine( mode='FAILURE', data='Error on stream open') if self.incoming_stream_version() == '1.0': if not self.Dispatcher.Stream.features: on_next_receive('RECEIVE_STREAM_FEATURES') else: self._xmpp_connect_machine(mode='STREAM_STARTED') else: self._xmpp_connect_machine(mode='STREAM_STARTED') elif mode == 'RECEIVE_STREAM_FEATURES': if data: # sometimes are received together with document # attributes and sometimes on next receive... self.Dispatcher.ProcessNonBlocking(data) if not self.Dispatcher.Stream.features: self._xmpp_connect_machine( mode='FAILURE', data='Missing in 1.0 stream') else: self._xmpp_connect_machine(mode='STREAM_STARTED') elif mode == 'STREAM_STARTED': self._on_stream_start() def _on_stream_start(self): '''Called when stream is opened. To be overriden in derived classes.''' def _on_connect_failure(self, retry=None, err_message=None): self.connected = None if err_message: log.debug('While connecting: %s' % err_message) if self.socket: self.socket.disconnect() self.on_connect_failure(retry) def _on_connect(self): self.onreceive(None) self.on_connect(self, self.connected) # moved from client.CommonClient: 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): """ Returns connection state. F.e.: None / 'tls' / 'tcp+non_sasl' . """ return self.connected def get_peerhost(self): ''' get the ip address of the account, from which is made connection to the server , (e.g. me). We will create listening socket on the same ip ''' if hasattr(self, 'Connection'): return self.Connection._sock.getsockname() def auth(self, user, password, resource = '', sasl = 1, on_auth = None): print 'auth called' ''' Authenticate connnection and bind resource. If resource is not provided random one or library name used. ''' self._User, self._Password, self._Resource, self._sasl = user, password, resource, sasl self.on_auth = on_auth self._on_doc_attrs() return def _on_old_auth(self, res): if res: self.connected += '+old_auth' self.on_auth(self, 'old_auth') else: self.on_auth(self, None) def _on_doc_attrs(self): if self._sasl: auth_nb.SASL(self._User, self._Password, self._on_start_sasl).PlugIn(self) if not self._sasl or self.SASL.startsasl == 'not-supported': if not self._Resource: self._Resource = 'xmpppy' auth_nb.NonBlockingNonSASL(self._User, self._Password, self._Resource, self._on_old_auth).PlugIn(self) return self.onreceive(self._on_start_sasl) self.SASL.auth() return True def _on_start_sasl(self, data=None): if data: self.Dispatcher.ProcessNonBlocking(data) if not self.__dict__.has_key('SASL'): # SASL is pluged out, possible disconnect return if self.SASL.startsasl == 'in-process': return self.onreceive(None) if self.SASL.startsasl == 'failure': # wrong user/pass, stop auth self.connected = None self._on_sasl_auth(None) self.SASL.PlugOut() elif self.SASL.startsasl == 'success': auth_nb.NonBlockingBind().PlugIn(self) self.onreceive(self._on_auth_bind) return True def _on_auth_bind(self, data): if data: self.Dispatcher.ProcessNonBlocking(data) if self.NonBlockingBind.bound is None: return self.NonBlockingBind.NonBlockingBind(self._Resource, self._on_sasl_auth) return True def _on_sasl_auth(self, res): self.onreceive(None) if res: self.connected += '+sasl' self.on_auth(self, 'sasl') else: self.on_auth(self, None) class NonBlockingClient(NBCommonClient): ''' Example client class, based on CommonClient. ''' def _on_stream_start(self): ''' Called after XMPP stream is opened. In pure XMPP client, TLS negotiation may follow after esabilishing a stream. ''' self.onreceive(None) if self.connected == 'tcp': if not self.connected or self._secure is not None and not self._secure: # if we are disconnected or TLS/SSL is not desired, return self._on_connect() return if not self.Dispatcher.Stream.features.getTag('starttls'): # if server doesn't advertise TLS in init response self._on_connect() return if self.incoming_stream_version() != '1.0': self._on_connect() return # otherwise start TLS tls_nb.NonBlockingTLS().PlugIn( self, on_tls_success=lambda: self._xmpp_connect(socket_type='tls'), on_tls_failure=self._on_connect_failure) elif self.connected == 'tls': self._on_connect() def initRoster(self): ''' Plug in the roster. ''' if not self.__dict__.has_key('NonBlockingRoster'): roster_nb.NonBlockingRoster().PlugIn(self) def getRoster(self, on_ready = None): ''' Return the Roster instance, previously plugging it in and requesting roster from server if needed. ''' if self.__dict__.has_key('NonBlockingRoster'): return self.NonBlockingRoster.getRoster(on_ready) 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: roster_nb.NonBlockingRoster().PlugIn(self) self.send(dispatcher_nb.Presence(to=jid, typ=typ))