diff --git a/CHANGELOG b/CHANGELOG
index f6ff3162..8e135e63 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,7 +1,8 @@
All theses changelogs are not exhaustive, please check the Mercurial repository for more details.
v 0.7.0 (NOT RELEASED YET):
- This version is a huge gap with previous one, changelog only show a part of novelties
+ This version is a huge gap with previous one, changelog only show a part of novelties.
+ This is also the first "general audience" version.
- XEP-0070 implementation (HTTP Auth via XMPP)
- XEP-0184 implementation (Delivery Receipts)
- XEP-0231 implementation (Bits of Binary)
@@ -29,6 +30,7 @@ v 0.7.0 (NOT RELEASED YET):
- language detection plugin
- media remote control through MPRIS standard
- generic encryption methods handling in core
+ - server certificate validation
- jp:
- new debug commands, to monitor stream, call bridge method or send fake signals
- new info/session command, to get data on current session
diff --git a/sat/core/exceptions.py b/sat/core/exceptions.py
index cb80b38c..9b1c235a 100644
--- a/sat/core/exceptions.py
+++ b/sat/core/exceptions.py
@@ -121,3 +121,8 @@ class ParsingError(Exception):
# Something which need to be done is not available yet
class NotReady(Exception):
pass
+
+
+class InvalidCertificate(Exception):
+ """A TLS certificate is not valid"""
+ pass
diff --git a/sat/core/patches.py b/sat/core/patches.py
new file mode 100644
index 00000000..8c70c264
--- /dev/null
+++ b/sat/core/patches.py
@@ -0,0 +1,63 @@
+from twisted.words.protocols.jabber import xmlstream
+from twisted.internet import ssl
+from wokkel import client
+
+"""This module apply monkey patches to Twisted and Wokkel to handle certificate validation
+ during XMPP connection"""
+
+
+class TLSInitiatingInitializer(xmlstream.TLSInitiatingInitializer):
+ check_certificate = True
+
+ def onProceed(self, obj):
+ self.xmlstream.removeObserver('/failure', self.onFailure)
+ trustRoot = ssl.platformTrust() if self.check_certificate else None
+ ctx = ssl.CertificateOptions(trustRoot=trustRoot)
+ self.xmlstream.transport.startTLS(ctx)
+ self.xmlstream.reset()
+ self.xmlstream.sendHeader()
+ self._deferred.callback(xmlstream.Reset)
+
+
+class XMPPClient(client.XMPPClient):
+
+ def __init__(self, jid, password, host=None, port=5222, check_certificate=True):
+ self.jid = jid
+ self.domain = jid.host.encode('idna')
+ self.host = host
+ self.port = port
+
+ factory = HybridClientFactory(jid, password, check_certificate)
+
+ client.StreamManager.__init__(self, factory)
+
+
+def HybridClientFactory(jid, password, check_certificate=True):
+ a = HybridAuthenticator(jid, password, check_certificate)
+
+ return xmlstream.XmlStreamFactory(a)
+
+
+class HybridAuthenticator(client.HybridAuthenticator):
+
+ def __init__(self, jid, password, check_certificate):
+ xmlstream.ConnectAuthenticator.__init__(self, jid.host)
+ self.jid = jid
+ self.password = password
+ self.check_certificate = check_certificate
+
+ def associateWithStream(self, xs):
+ xmlstream.ConnectAuthenticator.associateWithStream(self, xs)
+
+ tlsInit = xmlstream.TLSInitiatingInitializer(xs)
+ tlsInit.check_certificate = self.check_certificate
+ xs.initializers = [client.client.CheckVersionInitializer(xs),
+ tlsInit,
+ client.CheckAuthInitializer(xs)]
+
+
+def apply():
+ xmlstream.TLSInitiatingInitializer = TLSInitiatingInitializer
+ client.XMPPClient = XMPPClient
+ client.HybridClientFactory = HybridClientFactory
+ client.HybridAuthenticator = HybridAuthenticator
diff --git a/sat/core/sat_main.py b/sat/core/sat_main.py
index 82a6464b..4947fcfb 100644
--- a/sat/core/sat_main.py
+++ b/sat/core/sat_main.py
@@ -19,6 +19,8 @@
import sat
from sat.core.i18n import _, languageSwitch
+from sat.core import patches
+patches.apply()
from twisted.application import service
from twisted.internet import defer
from twisted.words.protocols.jabber import jid
diff --git a/sat/core/xmpp.py b/sat/core/xmpp.py
index 41ba5bf6..60c28294 100644
--- a/sat/core/xmpp.py
+++ b/sat/core/xmpp.py
@@ -35,6 +35,7 @@ from sat.core.log import getLogger
log = getLogger(__name__)
from sat.core import exceptions
from sat.memory import encryption
+from sat.tools import xml_tools
from zope.interface import implements
import time
import calendar
@@ -44,6 +45,7 @@ import sys
class SatXMPPEntity(object):
"""Common code for Client and Component"""
+ _reason = None # reason of disconnection
def __init__(self, host_app, profile, max_retries):
@@ -214,6 +216,11 @@ class SatXMPPEntity(object):
return
super(SatXMPPEntity, self)._authd(xmlstream)
+ if self._reason is not None:
+ # if we have had trouble to connect we can reset
+ # the exception as the connection is now working.
+ del self._reason
+
# the following Deferred is used to know when we are connected
# so we need to be set it to None when connection is lost
self._connected = defer.Deferred()
@@ -267,6 +274,11 @@ class SatXMPPEntity(object):
## connection ##
+ def _disconnected(self, reason):
+ # we have to save the reason of disconnection, otherwise it would be lost
+ self._reason = reason
+ super(SatXMPPEntity, self)._disconnected(reason)
+
def connectionLost(self, connector, reason):
try:
self.keep_alife.stop()
@@ -288,9 +300,21 @@ class SatXMPPEntity(object):
if not self.conn_deferred.called:
# FIXME: real error is not gotten here (e.g. if jid is not know by Prosody,
# we should have the real error)
- self.conn_deferred.errback(
- error.StreamError(u"Server unexpectedly closed the connection")
- )
+ if self._reason is None:
+ err = error.StreamError(u"Server unexpectedly closed the connection")
+ else:
+ err = self._reason
+ try:
+ if err.value.args[0][0][2] == "certificate verify failed":
+ err = exceptions.InvalidCertificate(
+ _(u"Your server certificate is not valid "
+ u"(its identity can't be checked).\n\n"
+ u"This should never happen and may indicate that "
+ u"somebody is trying to spy on you.\n"
+ u"Please contact your server administrator."))
+ except (IndexError, TypeError):
+ pass
+ self.conn_deferred.errback(err)
@defer.inlineCallbacks
def _cleanConnection(self, __):
@@ -544,16 +568,8 @@ class SatXMPPClient(SatXMPPEntity, wokkel_client.XMPPClient):
trigger_suffix = ""
is_component = False
- def __init__(
- self,
- host_app,
- profile,
- user_jid,
- password,
- host=None,
- port=C.XMPP_C2S_PORT,
- max_retries=C.XMPP_MAX_RETRIES,
- ):
+ def __init__(self, host_app, profile, user_jid, password, host=None,
+ port=C.XMPP_C2S_PORT, max_retries=C.XMPP_MAX_RETRIES):
# XXX: DNS SRV records are checked when the host is not specified.
# If no SRV record is found, the host is directly extracted from the JID.
self.started = time.time()
@@ -594,11 +610,24 @@ class SatXMPPClient(SatXMPPEntity, wokkel_client.XMPPClient):
.format(host_ori=user_jid.host, host=host, port=port)
)
+ self.check_certificate = host_app.memory.getParamA(
+ "check_certificate", "Connection", profile_key=profile)
+
wokkel_client.XMPPClient.__init__(
- self, user_jid, password, host or None, port or C.XMPP_C2S_PORT
+ self, user_jid, password, host or None, port or C.XMPP_C2S_PORT,
+ check_certificate = self.check_certificate
)
SatXMPPEntity.__init__(self, host_app, profile, max_retries)
+ if not self.check_certificate:
+ msg = (_(u"Certificate validation is deactivated, this is unsecure and "
+ u"somebody may be spying on you. If you have no good reason to disable "
+ u"certificate validation, please activate \"Check certificate\" in your "
+ u"settings in \"Connection\" tab."))
+ xml_tools.quickNote(host_app, self, msg, _(u"Security notice"),
+ level = C.XMLUI_DATA_LVL_WARNING)
+
+
def _getPluginsList(self):
for p in self.host_app.plugins.itervalues():
if C.PLUG_MODE_CLIENT in p._info[u"modes"]:
diff --git a/sat/memory/params.py b/sat/memory/params.py
index c0ad9fdf..3016a597 100644
--- a/sat/memory/params.py
+++ b/sat/memory/params.py
@@ -75,23 +75,25 @@ class Params(object):
+
""" % {
- "category_general": D_("General"),
- "category_connection": D_("Connection"),
- "history_param": C.HISTORY_LIMIT,
- "history_label": D_("Chat history limit"),
- "show_offline_contacts": C.SHOW_OFFLINE_CONTACTS,
- "show_offline_contacts_label": D_("Show offline contacts"),
- "show_empty_groups": C.SHOW_EMPTY_GROUPS,
- "show_empty_groups_label": D_("Show empty groups"),
- "force_server_param": C.FORCE_SERVER_PARAM,
- "force_port_param": C.FORCE_PORT_PARAM,
- "new_account_label": D_("Register new account"),
- "autoconnect_label": D_("Connect on frontend startup"),
- "autodisconnect_label": D_("Disconnect on frontend closure"),
+ u"category_general": D_(u"General"),
+ u"category_connection": D_(u"Connection"),
+ u"history_param": C.HISTORY_LIMIT,
+ u"history_label": D_(u"Chat history limit"),
+ u"show_offline_contacts": C.SHOW_OFFLINE_CONTACTS,
+ u"show_offline_contacts_label": D_(u"Show offline contacts"),
+ u"show_empty_groups": C.SHOW_EMPTY_GROUPS,
+ u"show_empty_groups_label": D_(u"Show empty groups"),
+ u"force_server_param": C.FORCE_SERVER_PARAM,
+ u"force_port_param": C.FORCE_PORT_PARAM,
+ u"new_account_label": D_(u"Register new account"),
+ u"autoconnect_label": D_(u"Connect on frontend startup"),
+ u"autodisconnect_label": D_(u"Disconnect on frontend closure"),
+ u"check_certificate_label": D_(u"Check certificate (don't uncheck if unsure)"),
}
def load_default_params(self):