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('gajim.c.x.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.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
74 """
75 Called on disconnection - disconnect callback is picked based on state of
76 the client.
77 """
78
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
98
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
111
112 if self.proxy:
113
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
121 if not stream_started:
122
123
124
125
126 log.debug('calling on_connect_failure cb')
127 self._caller.streamError = message
128 self.on_connect_failure()
129 else:
130
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
171
172
173
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
183
184
185
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
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
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
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
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
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
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
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
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
276 if not socket_type:
277 if self.Connection.ssl_lib:
278
279 socket_type = 'ssl'
280 else:
281
282 socket_type = 'plain'
283 self.connected = socket_type
284 self._xmpp_connect_machine()
285
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)
303 else:
304 self.onreceive(lambda _data:self._xmpp_connect_machine(mode, _data))
305
306 if not mode:
307
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
328
329
330
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
346
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
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
367 return
368 self.onreceive(None)
369
370 if self.connected == 'plain':
371 if self.desired_security == 'plain':
372
373 self._on_connect()
374 else:
375
376 if self.incoming_stream_version() != '1.0':
377
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
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
424
426 """
427 Preceed call of on_connect callback
428 """
429 self.onreceive(None)
430 self.on_connect(self, self.connected)
431
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
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
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
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
499
522
531
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):
557
558
559
560
561
563 """
564 Register handler that will be called on disconnect
565 """
566 self.disconnect_handlers.append(handler)
567
569 """
570 Unregister handler that is called on disconnect
571 """
572 self.disconnect_handlers.remove(handler)
573
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
583 """
584 Return connection state. F.e.: None / 'tls' / 'plain+non_sasl'
585 """
586 return self.connected
587
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
596
597 return self.socket.peerhost
598