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