root/branches/gajim_0.11.1/src/common/connection.py

Revision 8849, 41.7 kB (checked in by asterix, 14 months ago)

fix typo in a string

  • Property svn:eol-style set to LF
Line 
1##      common/connection.py
2##
3##
4## Copyright (C) 2003-2004 Vincent Hanquez <tab@snarc.org>
5## Copyright (C) 2003-2006 Yann Le Boulanger <asterix@lagaule.org>
6## Copyright (C) 2005-2006 Nikos Kouremenos <kourem@gmail.com>
7## Copyright (C) 2005-2006 Dimitur Kirov <dkirov@gmail.com>
8## Copyright (C) 2005-2006 Travis Shirk <travis@pobox.com>
9##
10## This program is free software; you can redistribute it and/or modify
11## it under the terms of the GNU General Public License as published
12## by the Free Software Foundation; version 2 only.
13##
14## This program is distributed in the hope that it will be useful,
15## but WITHOUT ANY WARRANTY; without even the implied warranty of
16## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17## GNU General Public License for more details.
18##
19
20
21import os
22import random
23
24try:
25        randomsource = random.SystemRandom()
26except:
27        randomsource = random.Random()
28        randomsource.seed()
29
30import signal
31if os.name != 'nt':
32        signal.signal(signal.SIGPIPE, signal.SIG_DFL)
33
34import common.xmpp
35from common import helpers
36from common import gajim
37from common import GnuPG
38from common import passwords
39from common import exceptions
40
41from connection_handlers import *
42USE_GPG = GnuPG.USE_GPG
43
44from common.rst_xhtml_generator import create_xhtml
45
46class Connection(ConnectionHandlers):
47        '''Connection class'''
48        def __init__(self, name):
49                ConnectionHandlers.__init__(self)
50                self.name = name
51                self.connected = 0 # offline
52                self.connection = None # xmpppy ClientCommon instance
53                # this property is used to prevent double connections
54                self.last_connection = None # last ClientCommon instance
55                self.is_zeroconf = False
56                self.gpg = None
57                self.status = ''
58                self.priority = gajim.get_priority(name, 'offline')
59                self.old_show = ''
60                # increase/decrease default timeout for server responses
61                self.try_connecting_for_foo_secs = 45
62                # holds the actual hostname to which we are connected
63                self.connected_hostname = None
64                self.time_to_reconnect = None
65                self.last_time_to_reconnect = None
66                self.new_account_info = None
67                self.bookmarks = []
68                self.annotations = {}
69                self.on_purpose = False
70                self.last_io = gajim.idlequeue.current_time()
71                self.last_sent = []
72                self.last_history_line = {}
73                self.password = passwords.get_password(name)
74                self.server_resource = gajim.config.get_per('accounts', name, 'resource')
75                if gajim.config.get_per('accounts', self.name, 'keep_alives_enabled'):
76                        self.keepalives = gajim.config.get_per('accounts', self.name,'keep_alive_every_foo_secs')
77                else:
78                        self.keepalives = 0
79                self.privacy_rules_supported = False
80                # Do we continue connection when we get roster (send presence,get vcard...)
81                self.continue_connect_info = None
82                # To know the groupchat jid associated with a sranza ID. Useful to
83                # request vcard or os info... to a real JID but act as if it comes from
84                # the fake jid
85                self.groupchat_jids = {} # {ID : groupchat_jid}
86                if USE_GPG:
87                        self.gpg = GnuPG.GnuPG(gajim.config.get('use_gpg_agent'))
88                        gajim.config.set('usegpg', True)
89                else:
90                        gajim.config.set('usegpg', False)
91               
92                self.on_connect_success = None
93                self.on_connect_failure = None
94                self.retrycount = 0
95                self.jids_for_auto_auth = [] # list of jid to auto-authorize
96                self.muc_jid = {} # jid of muc server for each transport type
97                self.available_transports = {} # list of available transports on this
98                # server {'icq': ['icq.server.com', 'icq2.server.com'], }
99                self.vcard_supported = True
100                self.private_storage_supported = True
101        # END __init__
102
103        def put_event(self, ev):
104                if gajim.handlers.has_key(ev[0]):
105                        gajim.handlers[ev[0]](self.name, ev[1])
106
107        def dispatch(self, event, data):
108                '''always passes account name as first param'''
109                self.put_event((event, data))
110
111
112        def _reconnect(self):
113                # Do not try to reco while we are already trying
114                self.time_to_reconnect = None
115                if self.connected < 2: # connection failed
116                        gajim.log.debug('reconnect')
117                        self.retrycount += 1
118                        signed = self.get_signed_msg(self.status)
119                        self.on_connect_auth = self._init_roster
120                        self.connect_and_init(self.old_show, self.status, signed)
121                else:
122                        # reconnect succeeded
123                        self.time_to_reconnect = None
124                        self.retrycount = 0
125       
126        # We are doing disconnect at so many places, better use one function in all
127        def disconnect(self, on_purpose = False):
128                self.on_purpose = on_purpose
129                self.connected = 0
130                self.time_to_reconnect = None
131                self.privacy_rules_supported = False
132                if self.connection:
133                        # make sure previous connection is completely closed
134                        gajim.proxy65_manager.disconnect(self.connection)
135                        self.connection.disconnect()
136                        self.last_connection = None
137                        self.connection = None
138       
139        def _disconnectedReconnCB(self):
140                '''Called when we are disconnected'''
141                gajim.log.debug('disconnectedReconnCB')
142                if gajim.account_is_connected(self.name):
143                        # we cannot change our status to offline or connecting
144                        # after we auth to server
145                        self.old_show = STATUS_LIST[self.connected]
146                self.connected = 0
147                self.dispatch('STATUS', 'offline')
148                if not self.on_purpose:
149                        self.disconnect()
150                        if gajim.config.get_per('accounts', self.name, 'autoreconnect'):
151                                self.connected = 1
152                                self.dispatch('STATUS', 'connecting')
153                                if gajim.status_before_autoaway[self.name]:
154                                        # We were auto away. So go back online
155                                        self.status = gajim.status_before_autoaway[self.name]
156                                        gajim.status_before_autoaway[self.name] = ''
157                                        self.old_show = 'online'
158                                # this check has moved from _reconnect method
159                                # do exponential backoff until 15 minutes,
160                                # then small linear increase
161                                if self.retrycount < 2 or self.last_time_to_reconnect is None:
162                                        self.last_time_to_reconnect = 5
163                                if self.last_time_to_reconnect < 800:
164                                        self.last_time_to_reconnect *= 1.5
165                                self.last_time_to_reconnect += randomsource.randint(0, 5)
166                                self.time_to_reconnect = int(self.last_time_to_reconnect)
167                                gajim.log.debug("Reconnect to %s in %ss", self.name, self.time_to_reconnect)
168                                gajim.idlequeue.set_alarm(self._reconnect_alarm,
169                                        self.time_to_reconnect)
170                        elif self.on_connect_failure:
171                                self.on_connect_failure()
172                                self.on_connect_failure = None
173                        else:
174                                # show error dialog
175                                self._connection_lost()
176                else:
177                        self.disconnect()
178                self.on_purpose = False
179        # END disconenctedReconnCB
180       
181        def _connection_lost(self):
182                self.disconnect(on_purpose = False)
183                self.dispatch('STATUS', 'offline')
184                self.dispatch('CONNECTION_LOST',
185                        (_('Connection with account "%s" has been lost') % self.name,
186                        _('Reconnect manually.')))
187
188        def _event_dispatcher(self, realm, event, data):
189                if realm == common.xmpp.NS_REGISTER:
190                        if event == common.xmpp.features_nb.REGISTER_DATA_RECEIVED:
191                                # data is (agent, DataFrom, is_form, error_msg)
192                                if self.new_account_info and \
193                                self.new_account_info['hostname'] == data[0]:
194                                        # it's a new account
195                                        if not data[1]: # wrong answer
196                                                self.dispatch('ACC_NOT_OK', (
197                                                        _('Transport %s answered wrongly to register request: %s')\
198                                                        % (data[0], data[3])))
199                                                return
200                                        req = data[1]
201                                        req['username'] = self.new_account_info['name']
202                                        req['password'] = self.new_account_info['password']
203                                        def _on_register_result(result):
204                                                if not common.xmpp.isResultNode(result):
205                                                        self.dispatch('ACC_NOT_OK', (result.getError()))
206                                                        return
207                                                self.password = self.new_account_info['password']
208                                                if USE_GPG:
209                                                        self.gpg = GnuPG.GnuPG(gajim.config.get('use_gpg_agent'))
210                                                        gajim.config.set('usegpg', True)
211                                                else:
212                                                        gajim.config.set('usegpg', False)
213                                                gajim.connections[self.name] = self
214                                                self.dispatch('ACC_OK', (self.new_account_info))
215                                                self.new_account_info = None
216                                                if self.connection:
217                                                        self.connection.UnregisterDisconnectHandler(self._on_new_account)
218                                                self.disconnect(on_purpose=True)
219                                        common.xmpp.features_nb.register(self.connection, data[0],
220                                                req, _on_register_result)
221                                        return
222                                if not data[1]: # wrong answer
223                                        self.dispatch('ERROR', (_('Invalid answer'),
224                                                _('Transport %s answered wrongly to register request: %s') % \
225                                                (data[0], data[3])))
226                                        return
227                                is_form = data[2]
228                                conf = data[1]
229                                self.dispatch('REGISTER_AGENT_INFO', (data[0], conf, is_form))
230                elif realm == common.xmpp.NS_PRIVACY:
231                        if event == common.xmpp.features_nb.PRIVACY_LISTS_RECEIVED:
232                                # data is (list)
233                                self.dispatch('PRIVACY_LISTS_RECEIVED', (data))
234                        elif event == common.xmpp.features_nb.PRIVACY_LIST_RECEIVED:
235                                # data is (resp)
236                                if not data:
237                                        return
238                                rules = []
239                                name = data.getTag('query').getTag('list').getAttr('name')
240                                for child in data.getTag('query').getTag('list').getChildren():
241                                        dict_item = child.getAttrs()
242                                        childs = []
243                                        if dict_item.has_key('type'):
244                                                for scnd_child in child.getChildren():
245                                                        childs += [scnd_child.getName()]
246                                                rules.append({'action':dict_item['action'],
247                                                        'type':dict_item['type'], 'order':dict_item['order'],
248                                                        'value':dict_item['value'], 'child':childs})
249                                        else:
250                                                for scnd_child in child.getChildren():
251                                                        childs.append(scnd_child.getName())
252                                                rules.append({'action':dict_item['action'],
253                                                        'order':dict_item['order'], 'child':childs})
254                                self.dispatch('PRIVACY_LIST_RECEIVED', (name, rules))
255                        elif event == common.xmpp.features_nb.PRIVACY_LISTS_ACTIVE_DEFAULT:
256                                # data is (dict)
257                                self.dispatch('PRIVACY_LISTS_ACTIVE_DEFAULT', (data))
258                elif realm == '':
259                        if event == common.xmpp.transports.DATA_RECEIVED:
260                                self.dispatch('STANZA_ARRIVED', unicode(data, errors = 'ignore'))
261                        elif event == common.xmpp.transports.DATA_SENT:
262                                self.dispatch('STANZA_SENT', unicode(data))
263
264        def select_next_host(self, hosts):
265                hosts_best_prio = []
266                best_prio = 65535
267                sum_weight = 0
268                for h in hosts:
269                        if h['prio'] < best_prio:
270                                hosts_best_prio = [h]
271                                best_prio = h['prio']
272                                sum_weight = h['weight']
273                        elif h['prio'] == best_prio:
274                                hosts_best_prio.append(h)
275                                sum_weight += h['weight']
276                if len(hosts_best_prio) == 1:
277                        return hosts_best_prio[0]
278                r = random.randint(0, sum_weight)
279                min_w = sum_weight
280                # We return the one for which has the minimum weight and weight >= r
281                for h in hosts_best_prio:
282                        if h['weight'] >= r:
283                                if h['weight'] <= min_w:
284                                        min_w = h['weight']
285                return h
286
287        def connect(self, data = None):
288                ''' Start a connection to the Jabber server.
289                Returns connection, and connection type ('tls', 'ssl', 'tcp', '')
290                data MUST contain hostname, usessl, proxy, use_custom_host,
291                custom_host (if use_custom_host), custom_port (if use_custom_host)'''
292                if self.connection:
293                        return self.connection, ''
294
295                if data:
296                        hostname = data['hostname']
297                        usessl = data['usessl']
298                        self.try_connecting_for_foo_secs = 45
299                        p = data['proxy']
300                        use_srv = True
301                        use_custom = data['use_custom_host']
302                        if use_custom:
303                                custom_h = data['custom_host']
304                                custom_p = data['custom_port']
305                else:
306                        hostname = gajim.config.get_per('accounts', self.name, 'hostname')
307                        usessl = gajim.config.get_per('accounts', self.name, 'usessl')
308                        self.try_connecting_for_foo_secs = gajim.config.get_per('accounts',
309                                self.name, 'try_connecting_for_foo_secs')
310                        p = gajim.config.get_per('accounts', self.name, 'proxy')
311                        use_srv = gajim.config.get_per('accounts', self.name, 'use_srv')
312                        use_custom = gajim.config.get_per('accounts', self.name,
313                                'use_custom_host')
314                        custom_h = gajim.config.get_per('accounts', self.name, 'custom_host')
315                        custom_p = gajim.config.get_per('accounts', self.name, 'custom_port')
316
317                # create connection if it doesn't already exist
318                self.connected = 1
319                if p and p in gajim.config.get_per('proxies'):
320                        proxy = {'host': gajim.config.get_per('proxies', p, 'host')}
321                        proxy['port'] = gajim.config.get_per('proxies', p, 'port')
322                        proxy['user'] = gajim.config.get_per('proxies', p, 'user')
323                        proxy['password'] = gajim.config.get_per('proxies', p, 'pass')
324                else:
325                        proxy = None
326
327                h = hostname
328                p = 5222
329                # autodetect [for SSL in 5223/443 and for TLS if broadcasted]
330                secur = None
331                if usessl:
332                        p = 5223
333                        secur = 1 # 1 means force SSL no matter what the port will be
334                        use_srv = False # wants ssl? disable srv lookup
335                if use_custom:
336                        h = custom_h
337                        p = custom_p
338                        use_srv = False
339
340                hosts = []
341                # SRV resolver
342                self._proxy = proxy
343                self._secure = secur
344                self._hosts = [ {'host': h, 'port': p, 'prio': 10, 'weight': 10} ]
345                self._hostname = hostname
346                if use_srv:
347                        # add request for srv query to the resolve, on result '_on_resolve'
348                        # will be called
349                        gajim.resolver.resolve('_xmpp-client._tcp.' + helpers.idn_to_ascii(h),
350                                self._on_resolve)
351                else:
352                        self._on_resolve('', [])
353
354        def _on_resolve(self, host, result_array):
355                # SRV query returned at least one valid result, we put it in hosts dict
356                if len(result_array) != 0:
357                        self._hosts = [i for i in result_array]
358                self.connect_to_next_host()
359
360        def connect_to_next_host(self, retry = False):
361                if len(self._hosts):
362                        if self.last_connection:
363                                self.last_connection.socket.disconnect()
364                                self.last_connection = None
365                                self.connection = None
366                        if gajim.verbose:
367                                con = common.xmpp.NonBlockingClient(self._hostname, caller = self,
368                                        on_connect = self.on_connect_success,
369                                        on_connect_failure = self.connect_to_next_host)
370                        else:
371                                con = common.xmpp.NonBlockingClient(self._hostname, debug = [], caller = self,
372                                        on_connect = self.on_connect_success,
373                                        on_connect_failure = self.connect_to_next_host)
374                        self.last_connection = con
375                        # increase default timeout for server responses
376                        common.xmpp.dispatcher_nb.DEFAULT_TIMEOUT_SECONDS = self.try_connecting_for_foo_secs
377                        con.set_idlequeue(gajim.idlequeue)
378                        host = self.select_next_host(self._hosts)
379                        self._current_host = host
380                        self._hosts.remove(host)
381
382                        # FIXME: this is a hack; need a better way
383                        if self.on_connect_success == self._on_new_account:
384                                con.RegisterDisconnectHandler(self._on_new_account)
385
386                        con.connect((host['host'], host['port']), proxy = self._proxy,
387                                secure = self._secure)
388                        return
389                else:
390                        if not retry and self.retrycount == 0:
391                                self.time_to_reconnect = None
392                                if self.on_connect_failure:
393                                        self.on_connect_failure()
394                                        self.on_connect_failure = None
395                                else:
396                                        # shown error dialog
397                                        self._connection_lost()
398                        else:
399                                # try reconnect if connection has failed before auth to server
400                                self._disconnectedReconnCB()
401
402        def _connect_failure(self, con_type = None):
403                if not con_type:
404                        # we are not retrying, and not conecting
405                        if not self.retrycount and self.connected != 0:
406                                self.disconnect(on_purpose = True)
407                                self.dispatch('STATUS', 'offline')
408                                self.dispatch('CONNECTION_LOST',
409                                        (_('Could not connect to "%s"') % self._hostname,
410                                        _('Check your connection or try again later.')))
411
412        def _connect_success(self, con, con_type):
413                if not self.connected: # We went offline during connecting process
414                        # FIXME - not possible, maybe it was when we used threads
415                        return
416                self.hosts = []
417                if not con_type:
418                        gajim.log.debug('Could not connect to %s:%s' % (self._current_host['host'],
419                                self._current_host['port']))
420                self.connected_hostname = self._current_host['host']
421                self.on_connect_failure = None
422                con.RegisterDisconnectHandler(self._disconnectedReconnCB)
423                gajim.log.debug(_('Connected to server %s:%s with %s') % (self._current_host['host'],
424                        self._current_host['port'], con_type))
425                self._register_handlers(con, con_type)
426                return True
427
428        def _register_handlers(self, con, con_type):
429                self.peerhost = con.get_peerhost()
430                # notify the gui about con_type
431                self.dispatch('CON_TYPE', con_type)
432                ConnectionHandlers._register_handlers(self, con, con_type)
433                name = gajim.config.get_per('accounts', self.name, 'name')
434                hostname = gajim.config.get_per('accounts', self.name, 'hostname')
435                self.connection = con
436                con.auth(name, self.password, self.server_resource, 1, self.__on_auth)
437
438        def __on_auth(self, con, auth):
439                if not con:
440                        self.disconnect(on_purpose = True)
441                        self.dispatch('STATUS', 'offline')
442                        self.dispatch('CONNECTION_LOST',
443                                (_('Could not connect to "%s"') % self._hostname,
444                                _('Check your connection or try again later')))
445                        if self.on_connect_auth:
446                                self.on_connect_auth(None)
447                                self.on_connect_auth = None
448                                return
449                if not self.connected: # We went offline during connecting process
450                        if self.on_connect_auth:
451                                self.on_connect_auth(None)
452                                self.on_connect_auth = None
453                                return
454                if hasattr(con, 'Resource'):
455                        self.server_resource = con.Resource
456                if auth:
457                        self.last_io = gajim.idlequeue.current_time()
458                        self.connected = 2
459                        self.retrycount = 0
460                        if self.on_connect_auth:
461                                self.on_connect_auth(con)
462                                self.on_connect_auth = None
463                else:
464                        # Forget password, it's wrong
465                        self.password = None
466                        gajim.log.debug("Couldn't authenticate to %s" % self._hostname)
467                        self.disconnect(on_purpose = True)
468                        self.dispatch('STATUS', 'offline')
469                        self.dispatch('ERROR', (_('Authentication failed with "%s"') % self._hostname,
470                                _('Please check your login and password for correctness.')))
471                        if self.on_connect_auth:
472                                self.on_connect_auth(None)
473                                self.on_connect_auth = None
474        # END connect
475
476        def quit(self, kill_core):
477                if kill_core and gajim.account_is_connected(self.name):
478                        self.disconnect(on_purpose = True)
479       
480        def get_privacy_lists(self):
481                if not self.connection:
482                        return
483                common.xmpp.features_nb.getPrivacyLists(self.connection)
484
485        def get_active_default_lists(self):
486                if not self.connection:
487                        return
488                common.xmpp.features_nb.getActiveAndDefaultPrivacyLists(self.connection)
489       
490        def del_privacy_list(self, privacy_list):
491                if not self.connection:
492                        return
493                def _on_del_privacy_list_result(result):
494                        if result:
495                                self.dispatch('PRIVACY_LIST_REMOVED', privacy_list)
496                        else:
497                                self.dispatch('ERROR', (_('Error while removing privacy list'),
498                                        _('Privacy list %s has not been removed. It is maybe active in '
499                                        'one of your connected resources. Deactivate it and try '
500                                        'again.') % privacy_list))
501                common.xmpp.features_nb.delPrivacyList(self.connection, privacy_list,
502                        _on_del_privacy_list_result)
503       
504        def get_privacy_list(self, title):
505                if not self.connection:
506                        return
507                common.xmpp.features_nb.getPrivacyList(self.connection, title)
508       
509        def set_privacy_list(self, listname, tags):
510                if not self.connection:
511                        return
512                common.xmpp.features_nb.setPrivacyList(self.connection, listname, tags)
513       
514        def set_active_list(self, listname):
515                if not self.connection:
516                        return
517                common.xmpp.features_nb.setActivePrivacyList(self.connection, listname, 'active')
518
519        def set_default_list(self, listname):
520                if not self.connection:
521                        return
522                common.xmpp.features_nb.setDefaultPrivacyList(self.connection, listname)
523       
524        def build_privacy_rule(self, name, action):
525                '''Build a Privacy rule stanza for invisibility'''
526                iq = common.xmpp.Iq('set', common.xmpp.NS_PRIVACY, xmlns = '')
527                l = iq.getTag('query').setTag('list', {'name': name})
528                i = l.setTag('item', {'action': action, 'order': '1'})
529                i.setTag('presence-out')
530                return iq
531
532        def activate_privacy_rule(self, name):
533                if not self.connection:
534                        return
535                '''activate a privacy rule'''
536                iq = common.xmpp.Iq('set', common.xmpp.NS_PRIVACY, xmlns = '')
537                iq.getTag('query').setTag('active', {'name': name})
538                self.connection.send(iq)
539
540        def send_invisible_presence(self, msg, signed, initial = False):
541                # try to set the privacy rule
542                iq = self.build_privacy_rule('invisible', 'deny')
543                self.connection.SendAndCallForResponse(iq<