Enforce a Qt with SSL support.

This commit is contained in:
Florian Bruhin 2015-06-07 10:46:47 +02:00
parent e98a05e53d
commit a82b0d007d
3 changed files with 84 additions and 97 deletions

View File

@ -23,14 +23,8 @@ import collections
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication, from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication,
QUrl) QUrl)
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QSslError from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslError,
QSslSocket)
try:
from PyQt5.QtNetwork import QSslSocket
except ImportError:
SSL_AVAILABLE = False
else:
SSL_AVAILABLE = QSslSocket.supportsSsl()
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils, from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils,
@ -46,13 +40,12 @@ _proxy_auth_cache = {}
def init(): def init():
"""Disable insecure SSL ciphers on old Qt versions.""" """Disable insecure SSL ciphers on old Qt versions."""
if SSL_AVAILABLE: if not qtutils.version_check('5.3.0'):
if not qtutils.version_check('5.3.0'): # Disable weak SSL ciphers.
# Disable weak SSL ciphers. # See https://codereview.qt-project.org/#/c/75943/
# See https://codereview.qt-project.org/#/c/75943/ good_ciphers = [c for c in QSslSocket.supportedCiphers()
good_ciphers = [c for c in QSslSocket.supportedCiphers() if c.usedBits() >= 128]
if c.usedBits() >= 128] QSslSocket.setDefaultCiphers(good_ciphers)
QSslSocket.setDefaultCiphers(good_ciphers)
class SslError(QSslError): class SslError(QSslError):
@ -107,10 +100,9 @@ class NetworkManager(QNetworkAccessManager):
} }
self._set_cookiejar() self._set_cookiejar()
self._set_cache() self._set_cache()
if SSL_AVAILABLE: self.sslErrors.connect(self.on_ssl_errors)
self.sslErrors.connect(self.on_ssl_errors) self._rejected_ssl_errors = collections.defaultdict(list)
self._rejected_ssl_errors = collections.defaultdict(list) self._accepted_ssl_errors = collections.defaultdict(list)
self._accepted_ssl_errors = collections.defaultdict(list)
self.authenticationRequired.connect(self.on_authentication_required) self.authenticationRequired.connect(self.on_authentication_required)
self.proxyAuthenticationRequired.connect( self.proxyAuthenticationRequired.connect(
self.on_proxy_authentication_required) self.on_proxy_authentication_required)
@ -181,76 +173,67 @@ class NetworkManager(QNetworkAccessManager):
request.deleteLater() request.deleteLater()
self.shutting_down.emit() self.shutting_down.emit()
if SSL_AVAILABLE: # pragma: no mccabe @pyqtSlot('QNetworkReply*', 'QList<QSslError>')
@pyqtSlot('QNetworkReply*', 'QList<QSslError>') def on_ssl_errors(self, reply, errors): # pragma: no mccabe
def on_ssl_errors(self, reply, errors): """Decide if SSL errors should be ignored or not.
"""Decide if SSL errors should be ignored or not.
This slot is called on SSL/TLS errors by the self.sslErrors signal. This slot is called on SSL/TLS errors by the self.sslErrors signal.
Args: Args:
reply: The QNetworkReply that is encountering the errors. reply: The QNetworkReply that is encountering the errors.
errors: A list of errors. errors: A list of errors.
""" """
errors = [SslError(e) for e in errors] errors = [SslError(e) for e in errors]
ssl_strict = config.get('network', 'ssl-strict') ssl_strict = config.get('network', 'ssl-strict')
if ssl_strict == 'ask': if ssl_strict == 'ask':
try: try:
host_tpl = urlutils.host_tuple(reply.url()) host_tpl = urlutils.host_tuple(reply.url())
except ValueError: except ValueError:
host_tpl = None host_tpl = None
is_accepted = False is_accepted = False
is_rejected = False is_rejected = False
else: else:
is_accepted = set(errors).issubset( is_accepted = set(errors).issubset(
self._accepted_ssl_errors[host_tpl]) self._accepted_ssl_errors[host_tpl])
is_rejected = set(errors).issubset( is_rejected = set(errors).issubset(
self._rejected_ssl_errors[host_tpl]) self._rejected_ssl_errors[host_tpl])
if is_accepted: if is_accepted:
reply.ignoreSslErrors() reply.ignoreSslErrors()
elif is_rejected: elif is_rejected:
pass
else:
err_string = '\n'.join('- ' + err.errorString() for err in
errors)
answer = self._ask('SSL errors - continue?\n{}'.format(
err_string), mode=usertypes.PromptMode.yesno,
owner=reply)
if answer:
reply.ignoreSslErrors()
d = self._accepted_ssl_errors
else:
d = self._rejected_ssl_errors
if host_tpl is not None:
d[host_tpl] += errors
elif ssl_strict:
pass pass
else: else:
for err in errors: err_string = '\n'.join('- ' + err.errorString() for err in
# FIXME we might want to use warn here (non-fatal error) errors)
# https://github.com/The-Compiler/qutebrowser/issues/114 answer = self._ask('SSL errors - continue?\n{}'.format(
message.error(self._win_id, err_string), mode=usertypes.PromptMode.yesno,
'SSL error: {}'.format(err.errorString())) owner=reply)
reply.ignoreSslErrors() if answer:
reply.ignoreSslErrors()
d = self._accepted_ssl_errors
else:
d = self._rejected_ssl_errors
if host_tpl is not None:
d[host_tpl] += errors
elif ssl_strict:
pass
else:
for err in errors:
# FIXME we might want to use warn here (non-fatal error)
# https://github.com/The-Compiler/qutebrowser/issues/114
message.error(self._win_id,
'SSL error: {}'.format(err.errorString()))
reply.ignoreSslErrors()
@pyqtSlot(QUrl) @pyqtSlot(QUrl)
def clear_rejected_ssl_errors(self, url): def clear_rejected_ssl_errors(self, url):
"""Clear the rejected SSL errors on a reload. """Clear the rejected SSL errors on a reload.
Args: Args:
url: The URL to remove. url: The URL to remove.
""" """
try: try:
del self._rejected_ssl_errors[url] del self._rejected_ssl_errors[url]
except KeyError: except KeyError:
pass
else:
@pyqtSlot(QUrl)
def clear_rejected_ssl_errors(self, _url):
"""Clear the rejected SSL errors on a reload.
Does nothing because SSL is unavailable.
"""
pass pass
@pyqtSlot('QNetworkReply', 'QAuthenticator') @pyqtSlot('QNetworkReply', 'QAuthenticator')
@ -334,11 +317,7 @@ class NetworkManager(QNetworkAccessManager):
A QNetworkReply. A QNetworkReply.
""" """
scheme = req.url().scheme() scheme = req.url().scheme()
if scheme == 'https' and not SSL_AVAILABLE: if scheme in self._scheme_handlers:
return networkreply.ErrorNetworkReply(
req, "SSL is not supported by the installed Qt library!",
QNetworkReply.ProtocolUnknownError, self)
elif scheme in self._scheme_handlers:
return self._scheme_handlers[scheme].createRequest( return self._scheme_handlers[scheme].createRequest(
op, req, outgoing_data) op, req, outgoing_data)

View File

@ -213,6 +213,19 @@ def check_qt_version():
_die(text) _die(text)
def check_ssl_support():
"""Check if SSL support is available."""
try:
from PyQt5.QtNetwork import QSslSocket
except ImportError:
ok = False
else:
ok = QSslSocket.supportsSsl()
if not ok:
text = "Fatal error: Your Qt is built without SSL support."
_die(text)
def check_libraries(): def check_libraries():
"""Check if all needed Python libraries are installed.""" """Check if all needed Python libraries are installed."""
modules = { modules = {
@ -288,6 +301,7 @@ def earlyinit(args):
# Now we can be sure QtCore is available, so we can print dialogs on # Now we can be sure QtCore is available, so we can print dialogs on
# errors, so people only using the GUI notice them as well. # errors, so people only using the GUI notice them as well.
check_qt_version() check_qt_version()
check_ssl_support()
remove_inputhook() remove_inputhook()
check_libraries() check_libraries()
init_log(args) init_log(args)

View File

@ -29,10 +29,7 @@ import collections
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, qVersion from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, qVersion
from PyQt5.QtWebKit import qWebKitVersion from PyQt5.QtWebKit import qWebKitVersion
try: from PyQt5.QtNetwork import QSslSocket
from PyQt5.QtNetwork import QSslSocket
except ImportError:
QSslSocket = None
import qutebrowser import qutebrowser
from qutebrowser.utils import log, utils from qutebrowser.utils import log, utils
@ -199,16 +196,13 @@ def version():
'Qt: {}, runtime: {}'.format(QT_VERSION_STR, qVersion()), 'Qt: {}, runtime: {}'.format(QT_VERSION_STR, qVersion()),
'PyQt: {}'.format(PYQT_VERSION_STR), 'PyQt: {}'.format(PYQT_VERSION_STR),
] ]
lines += _module_versions() lines += _module_versions()
if QSslSocket is not None and QSslSocket.supportsSsl():
ssl_version = QSslSocket.sslLibraryVersionString()
else:
ssl_version = 'unavailable'
lines += [ lines += [
'Webkit: {}'.format(qWebKitVersion()), 'Webkit: {}'.format(qWebKitVersion()),
'Harfbuzz: {}'.format(os.environ.get('QT_HARFBUZZ', 'system')), 'Harfbuzz: {}'.format(os.environ.get('QT_HARFBUZZ', 'system')),
'SSL: {}'.format(ssl_version), 'SSL: {}'.format(QSslSocket.sslLibraryVersionString()),
'', '',
'Frozen: {}'.format(hasattr(sys, 'frozen')), 'Frozen: {}'.format(hasattr(sys, 'frozen')),
'Platform: {}, {}'.format(platform.platform(), 'Platform: {}, {}'.format(platform.platform(),