Skip to content
Snippets Groups Projects
connection.py 70.4 KiB
Newer Older
roidelapluie's avatar
roidelapluie committed
# -*- coding:utf-8 -*-
roidelapluie's avatar
roidelapluie committed
## src/common/connection.py
roidelapluie's avatar
roidelapluie committed
## Copyright (C) 2003-2005 Vincent Hanquez <tab AT snarc.org>
## Copyright (C) 2003-2008 Yann Leboulanger <asterix AT lagaule.org>
## Copyright (C) 2005 Alex Mauer <hawke AT hawkesnest.net>
roidelapluie's avatar
roidelapluie committed
##                    Stéphan Kochen <stephan AT kochen.nl>
roidelapluie's avatar
roidelapluie committed
## Copyright (C) 2005-2006 Dimitur Kirov <dkirov AT gmail.com>
##                         Travis Shirk <travis AT pobox.com>
##                         Nikos Kouremenos <kourem AT gmail.com>
roidelapluie's avatar
roidelapluie committed
## Copyright (C) 2006 Junglecow J <junglecow AT gmail.com>
roidelapluie's avatar
roidelapluie committed
##                    Stefan Bethge <stefan AT lanpartei.de>
## Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
## Copyright (C) 2007 Tomasz Melcer <liori AT exroot.org>
##                    Julien Pivotto <roidelapluie AT gmail.com>
## Copyright (C) 2007-2008 Stephan Erb <steve-e AT h3c.de>
## Copyright (C) 2008 Brendan Taylor <whateley AT gmail.com>
##                    Jonathan Schleifer <js-gajim AT webkeks.org>
Yann Leboulanger's avatar
Yann Leboulanger committed
## This file is part of Gajim.
##
## Gajim is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published
Yann Leboulanger's avatar
Yann Leboulanger committed
## by the Free Software Foundation; version 3 only.
Yann Leboulanger's avatar
Yann Leboulanger committed
## Gajim is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
roidelapluie's avatar
roidelapluie committed
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
Yann Leboulanger's avatar
Yann Leboulanger committed
## You should have received a copy of the GNU General Public License
roidelapluie's avatar
roidelapluie committed
## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
Yann Leboulanger's avatar
Yann Leboulanger committed
##
Piotr Gaczkowski's avatar
Piotr Gaczkowski committed
import socket
Yann Leboulanger's avatar
Yann Leboulanger committed
import time
try:
	randomsource = random.SystemRandom()
except Exception:
	randomsource = random.Random()
	randomsource.seed()
if os.name != 'nt':
	signal.signal(signal.SIGPIPE, signal.SIG_DFL)
dkirov's avatar
dkirov committed

import common.xmpp
from common import helpers
Yann Leboulanger's avatar
Yann Leboulanger committed
from common import gajim
from common import GnuPG
from common import passwords
from common import exceptions

from connection_handlers import *
Piotr Gaczkowski's avatar
Piotr Gaczkowski committed
from string import Template
log = logging.getLogger('gajim.c.connection')
Yann Leboulanger's avatar
Yann Leboulanger committed
ssl_error = {
2: _("Unable to get issuer certificate"),
3: _("Unable to get certificate CRL"),
4: _("Unable to decrypt certificate's signature"),
5: _("Unable to decrypt CRL's signature"),
6: _("Unable to decode issuer public key"),
7: _("Certificate signature failure"),
8: _("CRL signature failure"),
9: _("Certificate is not yet valid"),
10: _("Certificate has expired"),
11: _("CRL is not yet valid"),
12: _("CRL has expired"),
13: _("Format error in certificate's notBefore field"),
14: _("Format error in certificate's notAfter field"),
15: _("Format error in CRL's lastUpdate field"),
16: _("Format error in CRL's nextUpdate field"),
17: _("Out of memory"),
steve-e's avatar
steve-e committed
18: _("Self signed certificate"),
19: _("Self signed certificate in certificate chain"),
20: _("Unable to get local issuer certificate"),
Yann Leboulanger's avatar
Yann Leboulanger committed
21: _("Unable to verify the first certificate"),
22: _("Certificate chain too long"),
23: _("Certificate revoked"),
24: _("Invalid CA certificate"),
25: _("Path length constraint exceeded"),
26: _("Unsupported certificate purpose"),
27: _("Certificate not trusted"),
28: _("Certificate rejected"),
29: _("Subject issuer mismatch"),
30: _("Authority and subject key identifier mismatch"),
31: _("Authority and issuer serial number mismatch"),
32: _("Key usage does not include certificate signing"),
50: _("Application verification failure")
class Connection(ConnectionHandlers):
nkour's avatar
nkour committed
	'''Connection class'''
		ConnectionHandlers.__init__(self)
		self.name = name
		# self.connected:
		# 0=>offline,
		# 1=>connection in progress,
		# 2=>authorised
		self.connected = 0
		self.connection = None # xmpppy ClientCommon instance
		# this property is used to prevent double connections
		self.last_connection = None # last ClientCommon instance
		# If we succeed to connect, remember it so next time we try (after a
		# disconnection) we try only this type.
		self.last_connection_type = None
		self.lang = None
		if locale.getdefaultlocale()[0]:
			self.lang = locale.getdefaultlocale()[0].split('_')[0]
		self.is_zeroconf = False
		self.gpg = None
		self.USE_GPG = False
		if gajim.HAVE_GPG:
			self.USE_GPG = True
Yann Leboulanger's avatar
Yann Leboulanger committed
			self.gpg = GnuPG.GnuPG(gajim.config.get('use_gpg_agent'))
		self.status = ''
		self.priority = gajim.get_priority(name, 'offline')
		self.old_show = ''
		# increase/decrease default timeout for server responses
		self.try_connecting_for_foo_secs = 45
		# holds the actual hostname to which we are connected
		self.connected_hostname = None
		self.time_to_reconnect = None
Yann Leboulanger's avatar
Yann Leboulanger committed
		self.new_account_info = None
Yann Leboulanger's avatar
Yann Leboulanger committed
		self.new_account_form = None
		self.last_io = gajim.idlequeue.current_time()
		self.password = passwords.get_password(name)
		self.server_resource = gajim.config.get_per('accounts', name, 'resource')
Yann Leboulanger's avatar
Yann Leboulanger committed
		# All valid resource substitution strings should be added to this hash.
Piotr Gaczkowski's avatar
Piotr Gaczkowski committed
		if self.server_resource:
			self.server_resource = Template(self.server_resource).safe_substitute({
				'hostname': socket.gethostname()
			})
		if gajim.config.get_per('accounts', self.name, 'keep_alives_enabled'):
			self.keepalives = gajim.config.get_per('accounts', self.name,
				'keep_alive_every_foo_secs')
		else:
			self.keepalives = 0
		if gajim.config.get_per('accounts', self.name, 'ping_alives_enabled'):
			self.pingalives = gajim.config.get_per('accounts', self.name,
				'ping_alive_every_foo_secs')
		else:
			self.pingalives = 0
Yann Leboulanger's avatar
Yann Leboulanger committed
		self.privacy_rules_supported = False
		# Used to ask privacy only once at connection
		self.privacy_rules_requested = False
Piotr Gaczkowski's avatar
Piotr Gaczkowski committed
		self.blocked_list = []
		self.blocked_contacts = []
		self.blocked_groups = []
		self.music_track_info = 0
		self.pubsub_supported = False
		self.pubsub_publish_options_supported = False
		self.pep_supported = False
		self.mood = {}
		self.tune = {}
		self.activity = {}
Yann Leboulanger's avatar
Yann Leboulanger committed
		# Do we continue connection when we get roster (send presence,get vcard..)
Yann Leboulanger's avatar
Yann Leboulanger committed
		self.continue_connect_info = None
		# Do we auto accept insecure connection
		self.connection_auto_accepted = False
		# To know the groupchat jid associated with a sranza ID. Useful to
		# request vcard or os info... to a real JID but act as if it comes from
		# the fake jid
		self.groupchat_jids = {} # {ID : groupchat_jid}
		self.on_connect_success = None
		self.retrycount = 0
		self.jids_for_auto_auth = [] # list of jid to auto-authorize
		self.muc_jid = {} # jid of muc server for each transport type
		self.available_transports = {} # list of available transports on this
		# server {'icq': ['icq.server.com', 'icq2.server.com'], }
		self.vcard_supported = False
Yann Leboulanger's avatar
Yann Leboulanger committed
		self.private_storage_supported = True
	# END __init__
	def put_event(self, ev):
		if ev[0] in gajim.handlers:
			log.debug('Sending %s event to GUI: %s' % (ev[0], ev[1:]))
			gajim.handlers[ev[0]](self.name, ev[1])
		'''always passes account name as first param'''
		self.put_event((event, data))

	def _reconnect(self):
		# Do not try to reco while we are already trying
		self.time_to_reconnect = None
junglecow's avatar
junglecow committed
			self.connected = 1
			self.dispatch('STATUS', 'connecting')
			self.retrycount += 1
			self.on_connect_auth = self._discover_server_at_connection
			self.connect_and_init(self.old_show, self.status, self.USE_GPG)
			# reconnect succeeded
			self.time_to_reconnect = None
			self.retrycount = 0
	# We are doing disconnect at so many places, better use one function in all
Yann Leboulanger's avatar
Yann Leboulanger committed
	def disconnect(self, on_purpose=False):
		gajim.interface.music_track_changed(None, None, self.name)
		self.on_purpose = on_purpose
		self.connected = 0
		self.time_to_reconnect = None
		self.privacy_rules_supported = False
		if self.connection:
			# make sure previous connection is completely closed
			gajim.proxy65_manager.disconnect(self.connection)
			self.connection.disconnect()
			self.last_connection = None
			self.connection = None
	def _disconnectedReconnCB(self):
nkour's avatar
nkour committed
		'''Called when we are disconnected'''
		log.info('disconnectedReconnCB called')
		if gajim.account_is_connected(self.name):
			# we cannot change our status to offline or connecting
			# after we auth to server
			self.old_show = gajim.SHOW_LIST[self.connected]
		self.connected = 0
		if not self.on_purpose:
Yann Leboulanger's avatar
Yann Leboulanger committed
			self.dispatch('STATUS', 'offline')
			self.disconnect()
			if gajim.config.get_per('accounts', self.name, 'autoreconnect'):
junglecow's avatar
junglecow committed
				self.connected = -1
				self.dispatch('STATUS', 'error')
				if gajim.status_before_autoaway[self.name]:
					# We were auto away. So go back online
					self.status = gajim.status_before_autoaway[self.name]
					gajim.status_before_autoaway[self.name] = ''
					self.old_show = 'online'
				# this check has moved from _reconnect method
				# do exponential backoff until 15 minutes,
				# then small linear increase
				if self.retrycount < 2 or self.last_time_to_reconnect is None:
					self.last_time_to_reconnect = 5
				if self.last_time_to_reconnect < 800:
					self.last_time_to_reconnect *= 1.5
				self.last_time_to_reconnect += randomsource.randint(0, 5)
				self.time_to_reconnect = int(self.last_time_to_reconnect)
				log.info("Reconnect to %s in %ss", self.name, self.time_to_reconnect)
				gajim.idlequeue.set_alarm(self._reconnect_alarm,
					self.time_to_reconnect)
			elif self.on_connect_failure:
				self.on_connect_failure()
				self.on_connect_failure = None
				self._connection_lost()
		else:
			self.disconnect()
		self.on_purpose = False
	# END disconenctedReconnCB
	def _connection_lost(self):
		self.disconnect(on_purpose = False)
		self.dispatch('STATUS', 'offline')
		self.dispatch('CONNECTION_LOST',
			(_('Connection with account "%s" has been lost') % self.name,
			_('Reconnect manually.')))
Yann Leboulanger's avatar
Yann Leboulanger committed
	def _event_dispatcher(self, realm, event, data):
		if realm == common.xmpp.NS_REGISTER:
			if event == common.xmpp.features_nb.REGISTER_DATA_RECEIVED:
				# data is (agent, DataFrom, is_form, error_msg)
Yann Leboulanger's avatar
Yann Leboulanger committed
				if self.new_account_info and \
				self.new_account_info['hostname'] == data[0]:
Yann Leboulanger's avatar
Yann Leboulanger committed
					# it's a new account
					if not data[1]: # wrong answer
						self.dispatch('ACC_NOT_OK', (
							_('Server %(name)s answered wrongly to register request: '
							'%(error)s') % {'name': data[0], 'error': data[3]}))
Piotr Gaczkowski's avatar
Piotr Gaczkowski committed
					is_form = data[2]
					conf = data[1]
Yann Leboulanger's avatar
Yann Leboulanger committed
					if self.new_account_form:
						def _on_register_result(result):
							if not common.xmpp.isResultNode(result):
								self.dispatch('ACC_NOT_OK', (result.getError()))
								return
							if gajim.HAVE_GPG:
								self.USE_GPG = True
Yann Leboulanger's avatar
Yann Leboulanger committed
								self.gpg = GnuPG.GnuPG(gajim.config.get(
									'use_gpg_agent'))
							self.dispatch('ACC_OK', (self.new_account_info))
							self.new_account_info = None
							self.new_account_form = None
							if self.connection:
								self.connection.UnregisterDisconnectHandler(
									self._on_new_account)
							self.disconnect(on_purpose=True)
						# it's the second time we get the form, we have info user
						# typed, so send them
						if is_form:
							#TODO: Check if form has changed
							iq = common.xmpp.Iq('set', common.xmpp.NS_REGISTER, to=self._hostname)
							iq.setTag('query').addChild(node=self.new_account_form)
							self.connection.SendAndCallForResponse(iq,
								_on_register_result)
						else:
							if self.new_account_form.keys().sort() != \
							conf.keys().sort():
								# requested config has changed since first connection
								self.dispatch('ACC_NOT_OK', (_(
									'Server %s provided a different registration form')\
									% data[0]))
								return
							common.xmpp.features_nb.register(self.connection,
								self._hostname, self.new_account_form,
								_on_register_result)
						return
					try:
						errnum = self.connection.Connection.ssl_errnum
					except AttributeError:
						errnum = -1 # we don't have an errnum
					ssl_msg = ''
					if errnum > 0:
						ssl_msg = ssl_error.get(errnum, _('Unknown SSL error: %d') % errnum)
Yann Leboulanger's avatar
Yann Leboulanger committed
					ssl_cert = ''
					if hasattr(self.connection.Connection, 'ssl_cert_pem'):
						ssl_cert = self.connection.Connection.ssl_cert_pem
					ssl_fingerprint = ''
					if hasattr(self.connection.Connection, 'ssl_fingerprint_sha1'):
						ssl_fingerprint = \
							self.connection.Connection.ssl_fingerprint_sha1
					self.dispatch('NEW_ACC_CONNECTED', (conf, is_form, ssl_msg,
Yann Leboulanger's avatar
Yann Leboulanger committed
					self.connection.UnregisterDisconnectHandler(
						self._on_new_account)
					self.disconnect(on_purpose=True)
				if not data[1]: # wrong answer
					self.dispatch('ERROR', (_('Invalid answer'),
						_('Transport %(name)s answered wrongly to register request: '
						'%(error)s') % {'name': data[0], 'error': data[3]}))
				conf = data[1]
				self.dispatch('REGISTER_AGENT_INFO', (data[0], conf, is_form))
		elif realm == common.xmpp.NS_PRIVACY:
			if event == common.xmpp.features_nb.PRIVACY_LISTS_RECEIVED:
				# data is (list)
				self.dispatch('PRIVACY_LISTS_RECEIVED', (data))
			elif event == common.xmpp.features_nb.PRIVACY_LIST_RECEIVED:
				# data is (resp)
				if not data:
					return
				rules = []
				name = data.getTag('query').getTag('list').getAttr('name')
				for child in data.getTag('query').getTag('list').getChildren():
					dict_item = child.getAttrs()
					childs = []
						for scnd_child in child.getChildren():
							childs += [scnd_child.getName()]
						rules.append({'action':dict_item['action'],
							'type':dict_item['type'], 'order':dict_item['order'],
							'value':dict_item['value'], 'child':childs})
					else:
						for scnd_child in child.getChildren():
							childs.append(scnd_child.getName())
						rules.append({'action':dict_item['action'],
							'order':dict_item['order'], 'child':childs})
				self.dispatch('PRIVACY_LIST_RECEIVED', (name, rules))
			elif event == common.xmpp.features_nb.PRIVACY_LISTS_ACTIVE_DEFAULT:
				# data is (dict)
				self.dispatch('PRIVACY_LISTS_ACTIVE_DEFAULT', (data))
			if event == common.xmpp.transports_nb.DATA_RECEIVED:
				self.dispatch('STANZA_ARRIVED', unicode(data, errors = 'ignore'))
			elif event == common.xmpp.transports_nb.DATA_SENT:
				self.dispatch('STANZA_SENT', unicode(data))
	def _select_next_host(self, hosts):
		'''Selects the next host according to RFC2782 p.3 based on it's
		priority. Chooses between hosts with the same priority randomly,
		where the probability of being selected is proportional to the weight
		of the host.'''

		hosts_by_prio = sorted(hosts, key=operator.itemgetter('prio'))

		try:
			lowest_prio = hosts_by_prio[0]['prio']
		except IndexError:
			raise ValueError("No hosts to choose from!")

		hosts_lowest_prio = [h for h in hosts_by_prio if h['prio'] == lowest_prio]

		if len(hosts_lowest_prio) == 1:
			return hosts_lowest_prio[0]
		else:
			rndint = random.randint(0, sum(h['weight'] for h in hosts_lowest_prio))
			weightsum = 0
			for host in sorted(hosts_lowest_prio, key=operator.itemgetter(
			'weight')):
				weightsum += host['weight']
				if weightsum >= rndint:
					return host
		''' Start a connection to the Jabber server.
		Returns connection, and connection type ('tls', 'ssl', 'plain', '')
Piotr Gaczkowski's avatar
Piotr Gaczkowski committed
		data MUST contain hostname, usessl, proxy, use_custom_host,
		custom_host (if use_custom_host), custom_port (if use_custom_host)'''
Yann Leboulanger's avatar
Yann Leboulanger committed
		if self.connection:
			return self.connection, ''
Yann Leboulanger's avatar
Yann Leboulanger committed

			self.try_connecting_for_foo_secs = 45
			p = data['proxy']
			use_srv = True
			use_custom = data['use_custom_host']
			if use_custom:
				custom_h = data['custom_host']
				custom_p = data['custom_port']
		else:
			hostname = gajim.config.get_per('accounts', self.name, 'hostname')
			usessl = gajim.config.get_per('accounts', self.name, 'usessl')
			self.try_connecting_for_foo_secs = gajim.config.get_per('accounts',
				self.name, 'try_connecting_for_foo_secs')
			p = gajim.config.get_per('accounts', self.name, 'proxy')
			use_srv = gajim.config.get_per('accounts', self.name, 'use_srv')
			use_custom = gajim.config.get_per('accounts', self.name,
				'use_custom_host')
			custom_h = gajim.config.get_per('accounts', self.name, 'custom_host')
			custom_p = gajim.config.get_per('accounts', self.name, 'custom_port')
Piotr Gaczkowski's avatar
Piotr Gaczkowski committed
		# create connection if it doesn't already exist
		self.connected = 1
Yann Leboulanger's avatar
Yann Leboulanger committed
		if p and p in gajim.config.get_per('proxies'):
			proxyptr = gajim.config.get_per('proxies', p)
			for key in proxyptr.keys():
				proxy[key] = proxyptr[key][1]
		elif gajim.config.get_per('accounts', self.name, 'use_env_http_proxy'):
			try:
				try:
					env_http_proxy = os.environ['HTTP_PROXY']
				except Exception:
					env_http_proxy = os.environ['http_proxy']
				env_http_proxy = env_http_proxy.strip('"')
				# Dispose of the http:// prefix
				env_http_proxy = env_http_proxy.split('://')
				env_http_proxy = env_http_proxy[len(env_http_proxy)-1]
				env_http_proxy = env_http_proxy.split('@')

				if len(env_http_proxy) == 2:
					login = env_http_proxy[0].split(':')
					addr = env_http_proxy[1].split(':')
				else:
					login = ['', '']
					addr = env_http_proxy[0].split(':')

				proxy = {'host': addr[0], 'type' : u'http', 'user':login[0]}

				if len(addr) == 2:
					proxy['port'] = addr[1]
				else:
					proxy['port'] = 3128

				if len(login) == 2:
					proxy['password'] = login[1]
				else:
					proxy['password'] = u''

			except Exception:
		h = hostname
		p = 5222
		ssl_p = 5223
#			use_srv = False # wants ssl? disable srv lookup
		self._proxy = proxy
		self._hosts = [ {'host': h, 'port': p, 'ssl_port': ssl_p, 'prio': 10,
			'weight': 10} ]
		self._hostname = hostname
		if use_srv:
			# add request for srv query to the resolve, on result '_on_resolve'
			# will be called
			gajim.resolver.resolve('_xmpp-client._tcp.' + helpers.idn_to_ascii(h),
				self._on_resolve)
		else:
			self._on_resolve('', [])
	def _on_resolve(self, host, result_array):
		# SRV query returned at least one valid result, we put it in hosts dict
		if len(result_array) != 0:
			self._hosts = [i for i in result_array]
			# Add ssl port
			ssl_p = 5223
			if gajim.config.get_per('accounts', self.name, 'use_custom_host'):
				ssl_p = gajim.config.get_per('accounts', self.name, 'custom_port')
			for i in self._hosts:
				i['ssl_port'] = ssl_p
		self._connect_to_next_host()
	def _connect_to_next_host(self, retry=False):
			# No config option exist when creating a new account
Yann Leboulanger's avatar
Yann Leboulanger committed
				self._connection_types = [self.last_connection_type]
			elif self.name in gajim.config.get_per('accounts'):
				self._connection_types = gajim.config.get_per('accounts', self.name,
					'connection_types').split()
			else:
				self._connection_types = ['tls', 'ssl', 'plain']

			if self._proxy and self._proxy['type']=='bosh':
				# with BOSH, we can't do TLS negotiation with <starttls>, we do only "plain"
				# connection and TLS with handshake right after TCP connecting ("ssl")
				scheme = common.xmpp.transports_nb.urisplit(self._proxy['bosh_uri'])[0]
				if scheme=='https':
					self._connection_types = ['ssl']
				else:
					self._connection_types = ['plain']
			host = self._select_next_host(self._hosts)
			self._current_host = host
			self._hosts.remove(host)
			if not retry and self.retrycount == 0:
				log.debug("Out of hosts, giving up connecting to %s", self.name)
				self.time_to_reconnect = None
				if self.on_connect_failure:
					self.on_connect_failure()
					self.on_connect_failure = None
					# shown error dialog
					self._connection_lost()
				# try reconnect if connection has failed before auth to server
				self._disconnectedReconnCB()
	def connect_to_next_type(self, retry=False):
		if len(self._connection_types):
			self._current_type = self._connection_types.pop(0)
			if self.last_connection:
				self.last_connection.socket.disconnect()
				self.last_connection = None
				self.connection = None
				# SSL (force TLS on different port than plain)
tomk's avatar
tomk committed
				# If we do TLS over BOSH, port of XMPP server should be the standard one
				# and TLS should be negotiated because TLS on 5223 is deprecated
tomk's avatar
tomk committed
				if self._proxy and self._proxy['type']=='bosh':
					port = self._current_host['port']
				else:
					port = self._current_host['ssl_port']
			elif self._current_type == 'tls':
				# TLS - negotiate tls after XMPP stream is estabilished
				port = self._current_host['port']
			elif self._current_type == 'plain':
				# plain connection on defined port

			cacerts = os.path.join(common.gajim.DATA_DIR, 'other', 'cacerts.pem')
			mycerts = common.gajim.MY_CACERTS
			secure_tuple = (self._current_type, cacerts, mycerts)
			con = common.xmpp.NonBlockingClient(
				domain=self._hostname,
				caller=self,
				idlequeue=gajim.idlequeue)

			self.last_connection = con
			# increase default timeout for server responses
			common.xmpp.dispatcher_nb.DEFAULT_TIMEOUT_SECONDS = self.try_connecting_for_foo_secs
			# FIXME: this is a hack; need a better way
			if self.on_connect_success == self._on_new_account:
				con.RegisterDisconnectHandler(self._on_new_account)
			self.log_hosttype_info(port)
				hostname=self._current_host['host'],
				port=port,
				on_connect=self.on_connect_success,
				on_proxy_failure=self.on_proxy_failure,
				on_connect_failure=self.connect_to_next_type,
				proxy=self._proxy,
			self._connect_to_next_host(retry)
	def log_hosttype_info(self, port):
		msg = '>>>>>> Connecting to %s [%s:%d], type = %s' % (self.name,
			self._current_host['host'], port, self._current_type)
		log.info(msg)
		if self._proxy:
			msg = '>>>>>> '
			if self._proxy['type']=='bosh':
				msg = '%s over BOSH %s' % (msg, self._proxy['bosh_uri'])
			if self._proxy['type'] in ['http','socks5'] or self._proxy['bosh_useproxy']:
				msg = '%s over proxy %s:%s' % (msg, self._proxy['host'], self._proxy['port'])
			log.info(msg)

	def _connect_failure(self, con_type=None):
			# we are not retrying, and not conecting
			if not self.retrycount and self.connected != 0:
				self.disconnect(on_purpose = True)
				self.dispatch('STATUS', 'offline')
				pritxt = _('Could not connect to "%s"') % self._hostname
				sectxt = _('Check your connection or try again later.')
					# show error dialog
					key = common.xmpp.NS_XMPP_STREAMS + ' ' + self.streamError
					if key in common.xmpp.ERRORS:
						sectxt2 = _('Server replied: %s') % common.xmpp.ERRORS[key][2]
						self.dispatch('ERROR', (pritxt, '%s\n%s' % (sectxt2, sectxt)))
						return
				# show popup
				self.dispatch('CONNECTION_LOST', (pritxt, sectxt))
	def on_proxy_failure(self, reason):
		log.error('Connection to proxy failed: %s' % reason)
		self.time_to_reconnect = None
		self.on_connect_failure = None
		self.disconnect(on_purpose = True)
		self.dispatch('STATUS', 'offline')
		self.dispatch('CONNECTION_LOST',
			(_('Connection to proxy failed'), reason))

	def _connect_success(self, con, con_type):
		if not self.connected: # We went offline during connecting process
			# FIXME - not possible, maybe it was when we used threads
			return
		_con_type = con_type
		if _con_type != self._current_type:
			log.info('Connecting to next type beacuse desired is %s and returned is %s'
				% (self._current_type, _con_type))
		con.RegisterDisconnectHandler(self._on_disconnected)
		if _con_type == 'plain' and gajim.config.get_per('accounts', self.name,
		'warn_when_plaintext_connection'):
			self.dispatch('PLAIN_CONNECTION', (con,))
			return True
		if _con_type in ('tls', 'ssl') and con.Connection.ssl_lib != 'PYOPENSSL' \
		and gajim.config.get_per('accounts', self.name,
		'warn_when_insecure_ssl_connection') and \
		not self.connection_auto_accepted:
			# Pyopenssl is not used
			self.dispatch('INSECURE_SSL_CONNECTION', (con, _con_type))
			return True
		return self.connection_accepted(con, con_type)

	def connection_accepted(self, con, con_type):
		if not con or not con.Connection:
			self.disconnect(on_purpose=True)
			self.dispatch('STATUS', 'offline')
			self.dispatch('CONNECTION_LOST',
				(_('Could not connect to account %s') % self.name,
				_('Connection with account %s has been lost. Retry connecting.') % \
				self.name))
			return
		self.hosts = []
		self.connected_hostname = self._current_host['host']
		self.on_connect_failure = None
		con.UnregisterDisconnectHandler(self._on_disconnected)
		con.RegisterDisconnectHandler(self._disconnectedReconnCB)
		log.debug('Connected to server %s:%s with %s' % (
			self._current_host['host'], self._current_host['port'], con_type))
		if gajim.config.get_per('accounts', self.name, 'anonymous_auth'):
			name = None
		else:
			name = gajim.config.get_per('accounts', self.name, 'name')
		hostname = gajim.config.get_per('accounts', self.name, 'hostname')
		self.connection = con
			errnum = con.Connection.ssl_errnum
			errnum = -1 # we don't have an errnum
		if errnum > 0 and str(errnum) not in gajim.config.get_per('accounts',
		self.name, 'ignore_ssl_errors'):
Yann Leboulanger's avatar
Yann Leboulanger committed
			text = _('The authenticity of the %s certificate could be invalid.') %\
				hostname
			if errnum in ssl_error:
				text += _('\nSSL Error: <b>%s</b>') % ssl_error[errnum]
Yann Leboulanger's avatar
Yann Leboulanger committed
			else:
				text += _('\nUnknown SSL error: %d') % errnum
			self.dispatch('SSL_ERROR', (text, errnum, con.Connection.ssl_cert_pem,
Yann Leboulanger's avatar
Yann Leboulanger committed
				con.Connection.ssl_fingerprint_sha1))
			return True
		if hasattr(con.Connection, 'ssl_fingerprint_sha1'):
			saved_fingerprint = gajim.config.get_per('accounts', self.name, 'ssl_fingerprint_sha1')
			if saved_fingerprint:
				# Check sha1 fingerprint
				if con.Connection.ssl_fingerprint_sha1 != saved_fingerprint:
					self.dispatch('FINGERPRINT_ERROR',
						(con.Connection.ssl_fingerprint_sha1,))
					return True
			else:
				gajim.config.set_per('accounts', self.name, 'ssl_fingerprint_sha1',
					con.Connection.ssl_fingerprint_sha1)
Yann Leboulanger's avatar
Yann Leboulanger committed
		self._register_handlers(con, con_type)
		con.auth(
			user=name,
			password=self.password,
			resource=self.server_resource,
			sasl=1,
Yann Leboulanger's avatar
Yann Leboulanger committed
	def ssl_certificate_accepted(self):
		if not self.connection:
			self.disconnect(on_purpose=True)
			self.dispatch('STATUS', 'offline')
			self.dispatch('CONNECTION_LOST',
				(_('Could not connect to account %s') % self.name,
				_('Connection with account %s has been lost. Retry connecting.') % \
				self.name))
			return
Yann Leboulanger's avatar
Yann Leboulanger committed
		name = gajim.config.get_per('accounts', self.name, 'name')
		self._register_handlers(self.connection, 'ssl')
		self.connection.auth(name, self.password, self.server_resource, 1,
			self.__on_auth)

	def _register_handlers(self, con, con_type):
		self.peerhost = con.get_peerhost()
		# notify the gui about con_type
		self.dispatch('CON_TYPE', con_type)
		ConnectionHandlers._register_handlers(self, con, con_type)

	def __on_auth(self, con, auth):
		if not con:
			self.dispatch('CONNECTION_LOST',
				(_('Could not connect to "%s"') % self._hostname,
				_('Check your connection or try again later')))
			if self.on_connect_auth:
				self.on_connect_auth(None)
				self.on_connect_auth = None
				return
		if not self.connected: # We went offline during connecting process
			if self.on_connect_auth:
				self.on_connect_auth(None)
				self.on_connect_auth = None
				return
		if hasattr(con, 'Resource'):
			self.server_resource = con.Resource
		if gajim.config.get_per('accounts', self.name, 'anonymous_auth'):
			# Get jid given by server
			gajim.config.set_per('accounts', self.name, 'name', con.User)
			self.last_io = gajim.idlequeue.current_time()
			self.connected = 2
			if self.on_connect_auth:
				self.on_connect_auth(con)
				self.on_connect_auth = None
Yann Leboulanger's avatar
Yann Leboulanger committed
			# Forget password, it's wrong
			self.password = None
			gajim.log.debug("Couldn't authenticate to %s" % self._hostname)
			self.disconnect(on_purpose = True)
			self.dispatch('STATUS', 'offline')
Yann Leboulanger's avatar
Yann Leboulanger committed
			self.dispatch('ERROR', (_('Authentication failed with "%s"') % \
				self._hostname,
nkour's avatar
nkour committed
				_('Please check your login and password for correctness.')))
			if self.on_connect_auth:
				self.on_connect_auth(None)
				self.on_connect_auth = None

	def quit(self, kill_core):
		if kill_core and gajim.account_is_connected(self.name):
Yann Leboulanger's avatar
Yann Leboulanger committed
			self.disconnect(on_purpose=True)

	def add_lang(self, stanza):
		if self.lang:
			stanza.setAttr('xml:lang', self.lang)

	def get_privacy_lists(self):
		if not self.connection:
			return
		common.xmpp.features_nb.getPrivacyLists(self.connection)

	def send_keepalive(self):
		# nothing received for the last foo seconds
		if self.connection:
			self.connection.send(' ')

	def sendPing(self, pingTo=None):
		'''Send XMPP Ping (XEP-0199) request. If pingTo is not set, ping is sent
		to server to detect connection failure at application level.'''
Piotr Gaczkowski's avatar
Piotr Gaczkowski committed
		if not self.connection:
			return
		id_ = self.connection.getAnID()
		if pingTo:
			to = pingTo.get_full_jid()
			self.dispatch('PING_SENT', (pingTo))
		else:
			to = gajim.config.get_per('accounts', self.name, 'hostname')
			self.awaiting_xmpp_ping_id = id_
Piotr Gaczkowski's avatar
Piotr Gaczkowski committed
		iq.addChild(name = 'ping', namespace = common.xmpp.NS_PING)
		iq.setID(id_)
Piotr Gaczkowski's avatar
Piotr Gaczkowski committed
		def _on_response(resp):
			timePong = time_time()
Piotr Gaczkowski's avatar
Piotr Gaczkowski committed
			if not common.xmpp.isResultNode(resp):
				self.dispatch('PING_ERROR', (pingTo))
				return
			timeDiff = round(timePong - timePing,2)
			self.dispatch('PING_REPLY', (pingTo, timeDiff))
		if pingTo:
			timePing = time_time()
			self.connection.SendAndCallForResponse(iq, _on_response)
		else:
			self.connection.send(iq)
			gajim.idlequeue.set_alarm(self.check_pingalive, gajim.config.get_per(
				'accounts', self.name, 'time_for_ping_alive_answer'))
Piotr Gaczkowski's avatar
Piotr Gaczkowski committed

	def get_active_default_lists(self):
		if not self.connection:
			return
		common.xmpp.features_nb.getActiveAndDefaultPrivacyLists(self.connection)
	def del_privacy_list(self, privacy_list):
		if not self.connection:
			return
		def _on_del_privacy_list_result(result):
			if result:
				self.dispatch('PRIVACY_LIST_REMOVED', privacy_list)
			else:
				self.dispatch('ERROR', (_('Error while removing privacy list'),
					_('Privacy list %s has not been removed. It is maybe active in '
					'one of your connected resources. Deactivate it and try '
					'again.') % privacy_list))
		common.xmpp.features_nb.delPrivacyList(self.connection, privacy_list,
			_on_del_privacy_list_result)
	def get_privacy_list(self, title):
		if not self.connection:
			return
		common.xmpp.features_nb.getPrivacyList(self.connection, title)
	def set_privacy_list(self, listname, tags):
		if not self.connection:
			return
		common.xmpp.features_nb.setPrivacyList(self.connection, listname, tags)
	def set_active_list(self, listname):
		if not self.connection:
			return
		common.xmpp.features_nb.setActivePrivacyList(self.connection, listname, 'active')

	def set_default_list(self, listname):
		if not self.connection:
			return
		common.xmpp.features_nb.setDefaultPrivacyList(self.connection, listname)
	def build_privacy_rule(self, name, action, order=1):
		'''Build a Privacy rule stanza for invisibility'''
		iq = common.xmpp.Iq('set', common.xmpp.NS_PRIVACY, xmlns = '')
		l = iq.getTag('query').setTag('list', {'name': name})
		i = l.setTag('item', {'action': action, 'order': str(order)})
	def build_invisible_rule(self):
		iq = common.xmpp.Iq('set', common.xmpp.NS_PRIVACY, xmlns = '')
		l = iq.getTag('query').setTag('list', {'name': 'invisible'})
		if self.name in gajim.interface.status_sent_to_groups and \
		len(gajim.interface.status_sent_to_groups[self.name]) > 0:
			for group in gajim.interface.status_sent_to_groups[self.name]:
				i = l.setTag('item', {'type': 'group', 'value': group,
					'action': 'allow', 'order': '1'})
				i.setTag('presence-out')
		if self.name in gajim.interface.status_sent_to_users and \
		len(gajim.interface.status_sent_to_users[self.name]) > 0:
			for jid in gajim.interface.status_sent_to_users[self.name]:
				i = l.setTag('item', {'type': 'jid', 'value': jid,
					'action': 'allow', 'order': '2'})
				i.setTag('presence-out')
		i = l.setTag('item', {'action': 'deny', 'order': '3'})
		i.setTag('presence-out')
		return iq

	def set_invisible_rule(self):
		if not gajim.account_is_connected(self.name):
			return
		iq = self.build_invisible_rule()
		self.connection.send(iq)

		if not self.connection:
			return
		iq = common.xmpp.Iq('set', common.xmpp.NS_PRIVACY, xmlns = '')
		iq.getTag('query').setTag('active', {'name': name})
		self.connection.send(iq)

	def send_invisible_presence(self, msg, signed, initial = False):
Piotr Gaczkowski's avatar
Piotr Gaczkowski committed
		if not self.connection:
			return
		if not self.privacy_rules_supported:
			self.dispatch('STATUS', gajim.SHOW_LIST[self.connected])
			self.dispatch('ERROR', (_('Invisibility not supported'),
				_('Account %s doesn\'t support invisibility.') % self.name))
			return
		# If we are already connected, and privacy rules are supported, send
		# offline presence first as it's required by XEP-0126
		if self.connected > 1 and self.privacy_rules_supported:
			self.on_purpose = True
			p = common.xmpp.Presence(typ = 'unavailable')
			p = self.add_sha(p, False)
			if msg:
				p.setStatus(msg)
			self.remove_all_transfers()
			self.connection.send(p)

		self.connection.SendAndCallForResponse(iq, self._continue_invisible,
			{'msg': msg, 'signed': signed, 'initial': initial})
	def _continue_invisible(self, con, iq_obj, msg, signed, initial):
		if iq_obj.getType() == 'error': # server doesn't support privacy lists
			return
		# active the privacy rule
		self.privacy_rules_supported = True
		self.activate_privacy_rule('invisible')
		self.connected = gajim.SHOW_LIST.index('invisible')
		self.status = msg
		priority = unicode(gajim.get_priority(self.name, 'invisible'))
		p = common.xmpp.Presence(priority = priority)
		p = self.add_sha(p, True)
		if msg:
			p.setStatus(msg)
		if signed:
			p.setTag(common.xmpp.NS_SIGNED + ' x').setData(signed)
		self.connection.send(p)
			self.request_vcard(None)

			#Get bookmarks from private namespace
			self.get_bookmarks()
			#Get annotations
			self.get_annotations()
			#Inform GUI we just signed in
nkour's avatar
nkour committed
			self.dispatch('SIGNED_IN', ())
	def test_gpg_passphrase(self, password):
		'''Returns 'ok', 'bad_pass' or 'expired' '''
Yann Leboulanger's avatar
Yann Leboulanger committed
		if not self.gpg:
			return False
		self.gpg.passphrase = password
		keyID = gajim.config.get_per('accounts', self.name, 'keyid')
		signed = self.gpg.sign('test', keyID)
		self.gpg.password = None
		if signed == 'KEYEXPIRED':
			return 'expired'
		elif signed == 'BAD_PASSPHRASE':
			return 'bad_pass'
		return 'ok'