1   
  2   
  3   
  4   
  5   
  6   
  7   
  8   
  9   
 10   
 11   
 12   
 13   
 14   
 15   
 16   
 17   
 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('nbxmpp.client_nb') 
 29   
 30   
 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  
 56   
 57           
 58           
 59          self._caller = caller 
 60          self._owner = self 
 61          self._registered_name = None  
 62          self.connected = '' 
 63          self.ip_addresses = [] 
 64          self.socket = None 
 65          self.on_connect = None 
 66          self.on_proxy_failure = None 
 67          self.on_connect_failure = None 
 68          self.proxy = None 
 69          self.got_features = False 
 70          self.got_see_other_host = None 
 71          self.stream_started = False 
 72          self.disconnecting = False 
 73          self.protocol_type = 'XMPP' 
  74   
 76          """ 
 77          Called on disconnection - disconnect callback is picked based on state of 
 78          the client. 
 79          """ 
 80           
 81          if self.ip_addresses: 
 82              self._try_next_ip() 
 83              return 
 84          if self.disconnecting: return 
 85   
 86          log.info('Disconnecting NBClient: %s' % message) 
 87   
 88          sasl_failed = False 
 89          if 'NonBlockingRoster' in self.__dict__: 
 90              self.NonBlockingRoster.PlugOut() 
 91          if 'NonBlockingBind' in self.__dict__: 
 92              self.NonBlockingBind.PlugOut() 
 93          if 'NonBlockingNonSASL' in self.__dict__: 
 94              self.NonBlockingNonSASL.PlugOut() 
 95          if 'SASL' in self.__dict__: 
 96              if 'startsasl' in self.SASL.__dict__ and \ 
 97              self.SASL.startsasl == 'failure-in-process': 
 98                  sasl_failed = True 
 99                  self.SASL.startsasl = 'failure' 
100                  self._on_start_sasl() 
101              else: 
102                  self.SASL.PlugOut() 
103          if 'NonBlockingTCP' in self.__dict__: 
104              self.NonBlockingTCP.PlugOut() 
105          if 'NonBlockingHTTP' in self.__dict__: 
106              self.NonBlockingHTTP.PlugOut() 
107          if 'NonBlockingBOSH' in self.__dict__: 
108              self.NonBlockingBOSH.PlugOut() 
109           
110           
111   
112          connected = self.connected 
113          stream_started = self.stream_started 
114   
115          self.connected = '' 
116          self.stream_started = False 
117   
118          self.disconnecting = True 
119   
120          log.debug('Client disconnected..') 
121           
122           
123          if connected == '' and not sasl_failed: 
124               
125               
126              if self.proxy: 
127                   
128                  log.debug('calling on_proxy_failure cb') 
129                  self.on_proxy_failure(reason=message) 
130              else: 
131                  log.debug('calling on_connect_failure cb') 
132                  self.on_connect_failure() 
133          elif not sasl_failed: 
134               
135              if not stream_started: 
136                   
137                   
138                   
139                   
140                  log.debug('calling on_connect_failure cb') 
141                  self._caller.streamError = message 
142                  self.on_connect_failure() 
143              else: 
144                   
145                  for i in reversed(self.disconnect_handlers): 
146                      log.debug('Calling disconnect handler %s' % i) 
147                      i() 
148          self.disconnecting = False 
 149   
150 -    def connect(self, on_connect, on_connect_failure, hostname=None, port=5222, 
151      on_proxy_failure=None, on_stream_error_cb=None, proxy=None, 
152      secure_tuple=('plain', None, None)): 
 153          """ 
154          Open XMPP connection (open XML streams in both directions) 
155   
156          :param on_connect: called after stream is successfully opened 
157          :param on_connect_failure: called when error occures during connection 
158          :param hostname: hostname of XMPP server from SRV request 
159          :param port: port number of XMPP server 
160          :param on_proxy_failure: called if error occurres during TCP connection to 
161                  proxy server or during proxy connecting process 
162          :param proxy: dictionary with proxy data. It should contain at least 
163                  values for keys 'host' and 'port' - connection details for proxy serve 
164                  and optionally keys 'user' and 'pass' as proxy credentials 
165          :param secure_tuple: tuple of (desired connection type, cacerts, mycerts) 
166                  connection type can be 'ssl' - TLS established after TCP connection, 
167                  'tls' - TLS established after negotiation with starttls, or 'plain'. 
168                  cacerts, mycerts - see tls_nb.NonBlockingTLS constructor for more 
169                  details 
170          """ 
171          self.on_connect = on_connect 
172          self.on_connect_failure=on_connect_failure 
173          self.on_proxy_failure = on_proxy_failure 
174          self.on_stream_error_cb = on_stream_error_cb 
175          self.desired_security, self.cacerts, self.mycerts = secure_tuple 
176          self.Connection = None 
177          self.Port = port 
178          self.proxy = proxy 
179   
180          if hostname: 
181              self.xmpp_hostname = hostname 
182          else: 
183              self.xmpp_hostname = self.Server 
184   
185           
186           
187           
188           
189          establish_tls = self.desired_security == 'ssl' 
190          certs = (self.cacerts, self.mycerts) 
191   
192          proxy_dict = {} 
193          tcp_host = self.xmpp_hostname 
194          tcp_port = self.Port 
195   
196          if proxy: 
197               
198               
199               
200               
201              tcp_host, tcp_port, proxy_user, proxy_pass = \ 
202                      transports_nb.get_proxy_data_from_dict(proxy) 
203   
204              if proxy['type'] == 'bosh': 
205                   
206                  self.socket = bosh.NonBlockingBOSH.get_instance( 
207                          on_disconnect=self.disconnect, 
208                          raise_event=self.raise_event, 
209                          idlequeue=self.idlequeue, 
210                          estabilish_tls=establish_tls, 
211                          certs=certs, 
212                          proxy_creds=(proxy_user, proxy_pass), 
213                          xmpp_server=(self.xmpp_hostname, self.Port), 
214                          domain=self.Server, 
215                          bosh_dict=proxy) 
216                  self.protocol_type = 'BOSH' 
217                  self.wait_for_restart_response = \ 
218                          proxy['bosh_wait_for_restart_response'] 
219              else: 
220                   
221                  proxy_dict['type'] = proxy['type'] 
222                  proxy_dict['xmpp_server'] = (self.xmpp_hostname, self.Port) 
223                  proxy_dict['credentials'] = (proxy_user, proxy_pass) 
224   
225          if not proxy or proxy['type'] != 'bosh': 
226               
227              self.socket = transports_nb.NonBlockingTCP.get_instance( 
228                      on_disconnect=self.disconnect, 
229                      raise_event=self.raise_event, 
230                      idlequeue=self.idlequeue, 
231                      estabilish_tls=establish_tls, 
232                      certs=certs, 
233                      proxy_dict=proxy_dict) 
234   
235           
236          self.socket.PlugIn(self) 
237   
238          self._resolve_hostname( 
239                  hostname=tcp_host, 
240                  port=tcp_port, 
241                  on_success=self._try_next_ip) 
 242   
244          """ 
245          Wrapper for getaddinfo call 
246   
247          FIXME: getaddinfo blocks 
248          """ 
249          try: 
250              self.ip_addresses = socket.getaddrinfo(hostname, port, 
251                      socket.AF_UNSPEC, socket.SOCK_STREAM) 
252          except socket.gaierror, (errnum, errstr): 
253              self.disconnect(message='Lookup failure for %s:%s, hostname: %s - %s' % 
254                       (self.Server, self.Port, hostname, errstr)) 
255          except socket.error , (errnum, errstr): 
256               
257              self.disconnect(message='General socket error for %s:%s, hostname: %s - %s' % 
258                       (self.Server, self.Port, hostname, errstr)) 
259          else: 
260              on_success() 
 261   
263          """ 
264          Iterate over IP addresses tries to connect to it 
265          """ 
266          if err_message: 
267              log.debug('While looping over DNS A records: %s' % err_message) 
268          if self.ip_addresses == []: 
269              msg = 'Run out of hosts for name %s:%s.' % (self.Server, self.Port) 
270              msg = msg + ' Error for last IP: %s' % err_message 
271              self.disconnect(msg) 
272          else: 
273              self.current_ip = self.ip_addresses.pop(0) 
274              self.socket.connect( 
275                      conn_5tuple=self.current_ip, 
276                      on_connect=lambda: self._xmpp_connect(), 
277                      on_connect_failure=self._try_next_ip) 
 278   
280          """ 
281          Get version of xml stream 
282          """ 
283          if 'version' in self.Dispatcher.Stream._document_attrs: 
284              return self.Dispatcher.Stream._document_attrs['version'] 
285          else: 
286              return None 
 287   
289          """ 
290          Start XMPP connecting process - open the XML stream. Is called after TCP 
291          connection is established or after switch to TLS when successfully 
292          negotiated with <starttls>. 
293          """ 
294           
295          if not socket_type: 
296              if self.Connection.ssl_lib: 
297                   
298                  socket_type = 'ssl' 
299              else: 
300                   
301                  socket_type = 'plain' 
302          self.connected = socket_type 
303          self._xmpp_connect_machine() 
 304   
306          """ 
307          Finite automaton taking care of stream opening and features tag handling. 
308          Calls _on_stream_start when stream is started, and disconnect() on 
309          failure. 
310          """ 
311          log.info('-------------xmpp_connect_machine() >> mode: %s, data: %s...' % 
312                  (mode, str(data)[:20])) 
313   
314          def on_next_receive(mode): 
315              """ 
316              Set desired on_receive callback on transport based on the state of 
317              connect_machine. 
318              """ 
319              log.info('setting %s on next receive' % mode) 
320              if mode is None: 
321                  self.onreceive(None)  
322              else: 
323                  self.onreceive(lambda _data:self._xmpp_connect_machine(mode, _data)) 
 324   
325          if not mode: 
326               
327              if self.__dict__.has_key('Dispatcher'): 
328                  self.Dispatcher.PlugOut() 
329                  self.got_features = False 
330              dispatcher_nb.Dispatcher.get_instance().PlugIn(self) 
331              on_next_receive('RECEIVE_DOCUMENT_ATTRIBUTES') 
332   
333          elif mode == 'FAILURE': 
334              self.disconnect('During XMPP connect: %s' % data) 
335   
336          elif mode == 'RECEIVE_DOCUMENT_ATTRIBUTES': 
337              if data: 
338                  self.Dispatcher.ProcessNonBlocking(data) 
339                  self.ip_addresses = [] 
340              if not hasattr(self, 'Dispatcher') or \ 
341                      self.Dispatcher.Stream._document_attrs is None: 
342                  self._xmpp_connect_machine( 
343                          mode='FAILURE', 
344                          data='Error on stream open') 
345                  return 
346   
347               
348               
349               
350               
351              if not self.connected: return 
352   
353              if self.incoming_stream_version() == '1.0': 
354                  if not self.got_features: 
355                      on_next_receive('RECEIVE_STREAM_FEATURES') 
356                  else: 
357                      log.info('got STREAM FEATURES in first recv') 
358                      self._xmpp_connect_machine(mode='STREAM_STARTED') 
359              else: 
360                  log.info('incoming stream version less than 1.0') 
361                  self._xmpp_connect_machine(mode='STREAM_STARTED') 
362   
363          elif mode == 'RECEIVE_STREAM_FEATURES': 
364              if data: 
365                   
366                   
367                  self.Dispatcher.ProcessNonBlocking(data) 
368              if self.got_see_other_host: 
369                  log.info('got see-other-host') 
370                  self.onreceive(None) 
371                  self.on_stream_error_cb(self, self.got_see_other_host) 
372              elif not self.got_features: 
373                  self._xmpp_connect_machine(mode='FAILURE', 
374                      data='Missing <features> in 1.0 stream') 
375              else: 
376                  log.info('got STREAM FEATURES in second recv') 
377                  self._xmpp_connect_machine(mode='STREAM_STARTED') 
378   
379          elif mode == 'STREAM_STARTED': 
380              self._on_stream_start() 
 381   
383          """ 
384          Called after XMPP stream is opened.     TLS negotiation may follow if 
385          supported and desired. 
386          """ 
387          self.stream_started = True 
388          if not hasattr(self, 'onreceive'): 
389               
390              return 
391          self.onreceive(None) 
392   
393          if self.connected == 'plain': 
394              if self.desired_security == 'plain': 
395                   
396                  self._on_connect() 
397              else: 
398                   
399                  if self.incoming_stream_version() != '1.0': 
400                       
401                      log.info('While connecting with type = "tls": stream version ' + 
402                              'is less than 1.0') 
403                      self._on_connect() 
404                      return 
405                  if self.Dispatcher.Stream.features.getTag('starttls'): 
406                       
407                      self.stream_started = False 
408                      log.info('TLS supported by remote server. Requesting TLS start.') 
409                      self._tls_negotiation_handler() 
410                  else: 
411                      log.info('While connecting with type = "tls": TLS unsupported ' + 
412                              'by remote server') 
413                      self._on_connect() 
414   
415          elif self.connected in ['ssl', 'tls']: 
416              self._on_connect() 
417          else: 
418              assert False, 'Stream opened for unsupported connection' 
 419   
447   
449          """ 
450          Preceed call of on_connect callback 
451          """ 
452          self.onreceive(None) 
453          self.on_connect(self, self.connected) 
 454   
456          """ 
457          Raise event to connection instance. DATA_SENT and DATA_RECIVED events 
458          are used in XML console to show XMPP traffic 
459          """ 
460          log.info('raising event from transport: :::::%s::::\n_____________\n%s\n_____________\n' % (event_type, data)) 
461          if hasattr(self, 'Dispatcher'): 
462              self.Dispatcher.Event('', event_type, data) 
 463   
464   
465   
466   
467   
468 -    def auth(self, user, password, resource='', sasl=True, on_auth=None): 
 469          """ 
470          Authenticate connnection and bind resource. If resource is not provided 
471          random one or library name used 
472   
473          :param user: XMPP username 
474          :param password: XMPP password 
475          :param resource: resource that shall be used for auth/connecting 
476          :param sasl: Boolean indicating if SASL shall be used. (default: True) 
477          :param on_auth: Callback, called after auth. On auth failure, argument 
478                  is None. 
479          """ 
480          self._User, self._Password = user, password 
481          self._Resource, self._sasl = resource, sasl 
482          self.on_auth = on_auth 
483          self._on_doc_attrs() 
484          return 
 485   
487          """ 
488          Callback used by NON-SASL auth. On auth failure, res is None 
489          """ 
490          if res: 
491              self.connected += '+old_auth' 
492              self.on_auth(self, 'old_auth') 
493          else: 
494              self.on_auth(self, None) 
 495   
497          """ 
498          Used internally. On auth failure, res is None 
499          """ 
500          self.onreceive(None) 
501          if res: 
502              self.connected += '+sasl' 
503              self.on_auth(self, 'sasl') 
504          else: 
505              self.on_auth(self, None) 
 506   
524   
557   
566   
573   
574 -    def getRoster(self, on_ready=None, force=False): 
 575          """ 
576          Return the Roster instance, previously plugging it in and requesting 
577          roster from server if needed 
578          """ 
579          if self.__dict__.has_key('NonBlockingRoster'): 
580              return self.NonBlockingRoster.getRoster(on_ready, force) 
581          return None 
 582   
583 -    def sendPresence(self, jid=None, typ=None, requestRoster=0): 
 592   
593   
594   
595   
596   
598          """ 
599          Register handler that will be called on disconnect 
600          """ 
601          self.disconnect_handlers.append(handler) 
 602   
604          """ 
605          Unregister handler that is called on disconnect 
606          """ 
607          self.disconnect_handlers.remove(handler) 
 608   
610          """ 
611          Default disconnect handler. Just raises an IOError. If you choosed to use 
612          this class in your production client, override this method or at least 
613          unregister it. 
614          """ 
615          raise IOError('Disconnected from server.') 
 616   
618          """ 
619          Return connection state. F.e.: None / 'tls' / 'plain+non_sasl' 
620          """ 
621          return self.connected 
 622   
624          """ 
625          Gets the ip address of the account, from which is made connection to the 
626          server (e.g. IP and port of socket) 
627   
628          We will create listening socket on the same ip 
629          """ 
630           
631           
632          return self.socket.peerhost 
 633