webengine: Initial SSL error support

This commit is contained in:
Florian Bruhin 2016-11-10 06:50:00 +01:00
parent 65625a9dea
commit 6697d692e1
10 changed files with 191 additions and 97 deletions

View File

@ -797,8 +797,6 @@ Valid values:
Default: +pass:[ask]+
This setting is only available with the QtWebKit backend.
[[network-dns-prefetch]]
=== dns-prefetch
Whether to try to pre-fetch DNS entries to speed up browsing.

View File

@ -75,6 +75,23 @@ class UnsupportedOperationError(WebTabError):
"""Raised when an operation is not supported with the given backend."""
class AbstractCertificateErrorWrapper:
"""A wrapper over an SSL/certificate error."""
def __init__(self, error):
self._error = error
def __str__(self):
raise NotImplementedError
def __repr__(self):
raise NotImplementedError
def is_overridable(self):
raise NotImplementedError
class TabData:
"""A simple namespace with a fixed set of attributes.

View File

@ -21,6 +21,8 @@
import html
import jinja2
from qutebrowser.config import config
from qutebrowser.utils import usertypes, message, log
@ -110,3 +112,48 @@ def javascript_alert(url, js_msg, abort_on):
html.escape(js_msg))
message.ask('Javascript alert', msg, mode=usertypes.PromptMode.alert,
abort_on=abort_on)
def ignore_certificate_errors(url, errors, abort_on):
"""Display a certificate error question.
Args:
url: The URL the errors happened in
errors: A list of QSslErrors or QWebEngineCertificateErrors
Return:
True if the error should be ignored, False otherwise.
"""
ssl_strict = config.get('network', 'ssl-strict')
log.webview.debug("Certificate errors {!r}, strict {}".format(
errors, ssl_strict))
for error in errors:
assert error.is_overridable(), repr(error)
if ssl_strict == 'ask':
err_template = jinja2.Template("""
Errors while loading <b>{{url.toDisplayString()}}</b>:<br/>
<ul>
{% for err in errors %}
<li>{{err}}</li>
{% endfor %}
</ul>
""".strip())
msg = err_template.render(url=url, errors=errors)
return message.ask(title="Certificate errors - continue?", text=msg,
mode=usertypes.PromptMode.yesno, default=False,
abort_on=abort_on)
elif ssl_strict is False:
log.webview.debug("ssl-strict is False, only warning about errors")
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('Certificate error: {}'.format(err))
return True
elif ssl_strict is True:
return False
else:
raise ValueError("Invalid ssl_strict value {!r}".format(ssl_strict))
raise AssertionError("Not reached")

View File

@ -30,7 +30,8 @@ from PyQt5.QtGui import QKeyEvent, QIcon
# pylint: disable=no-name-in-module,import-error,useless-suppression
from PyQt5.QtWidgets import QOpenGLWidget, QApplication
from PyQt5.QtWebEngineWidgets import (QWebEnginePage, QWebEngineScript,
QWebEngineProfile)
QWebEngineProfile,
QWebEngineCertificateError)
# pylint: enable=no-name-in-module,import-error,useless-suppression
from qutebrowser.browser import browsertab, mouse, shared
@ -38,7 +39,7 @@ from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
interceptor, webenginequtescheme,
webenginedownloads)
from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
objreg, message)
objreg, message, debug)
_qute_scheme_handler = None
@ -78,6 +79,26 @@ _JS_WORLD_MAP = {
}
class CertificateErrorWrapper(browsertab.AbstractCertificateErrorWrapper):
"""A wrapper over a QWebEngineCertificateError."""
def __init__(self, error):
self._error = error
def __str__(self):
return self._error.errorDescription()
def __repr__(self):
return utils.get_repr(
self, error=debug.qenum_key(QWebEngineCertificateError,
self._error.error()),
string=str(self))
def is_overridable(self):
return self._error.isOverridable()
class WebEnginePrinting(browsertab.AbstractPrinting):
"""QtWebEngine implementations related to printing."""

View File

@ -27,8 +27,9 @@ from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
# pylint: enable=no-name-in-module,import-error,useless-suppression
from qutebrowser.browser import shared
from qutebrowser.browser.webengine import webenginetab
from qutebrowser.config import config
from qutebrowser.utils import log, debug, usertypes, objreg, qtutils
from qutebrowser.utils import log, debug, usertypes, objreg, qtutils, jinja
class WebEngineView(QWebEngineView):
@ -107,7 +108,7 @@ class WebEnginePage(QWebEnginePage):
_is_shutting_down: Whether the page is currently shutting down.
Signals:
certificate_error: FIXME:qtwebengine
certificate_error: Emitted on certificate errors.
link_clicked: Emitted when a link was clicked on a page.
shutting_down: Emitted when the page is shutting down.
"""
@ -127,7 +128,29 @@ class WebEnginePage(QWebEnginePage):
def certificateError(self, error):
self.certificate_error.emit()
return super().certificateError(error)
url = error.url()
error = webenginetab.CertificateErrorWrapper(error)
# FIXME
error_page = jinja.render('error.html',
title="Error while loading page",
url=url.toDisplayString(), error=str(error),
icon='', qutescheme=False)
if not error.is_overridable():
log.webview.error("Non-overridable certificate error: "
"{}".format(error))
self.setHtml(error_page)
return False
ignore = shared.ignore_certificate_errors(
url, [error], abort_on=[self.loadStarted, self.shutting_down])
if not ignore:
log.webview.error("Certificate error: {}".format(error))
self.setHtml(error_page)
return ignore
def javaScriptConfirm(self, url, js_msg):
if self._is_shutting_down:

View File

@ -24,7 +24,6 @@ import collections
import netrc
import html
import jinja2
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, PYQT_VERSION, QCoreApplication,
QUrl, QByteArray)
from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslError,
@ -34,8 +33,9 @@ from qutebrowser.config import config
from qutebrowser.utils import (message, log, usertypes, utils, objreg, qtutils,
urlutils, debug)
from qutebrowser.browser import shared
from qutebrowser.browser.webkit.network import webkitqutescheme, networkreply
from qutebrowser.browser.webkit.network import filescheme
from qutebrowser.browser.webkit import webkittab
from qutebrowser.browser.webkit.network import (webkitqutescheme, networkreply,
filescheme)
HOSTBLOCK_ERROR_STRING = '%HOSTBLOCK%'
@ -112,24 +112,6 @@ def init():
QSslSocket.setDefaultCiphers(good_ciphers)
class SslError(QSslError):
"""A QSslError subclass which provides __hash__ on Qt < 5.4."""
def __hash__(self):
try:
# Qt >= 5.4
# pylint: disable=not-callable,useless-suppression
return super().__hash__()
except TypeError:
return hash((self.certificate().toDer(), self.error()))
def __repr__(self):
return utils.get_repr(
self, error=debug.qenum_key(QSslError, self.error()),
string=self.errorString())
class NetworkManager(QNetworkAccessManager):
"""Our own QNetworkAccessManager.
@ -203,6 +185,19 @@ class NetworkManager(QNetworkAccessManager):
self.setCache(cache)
cache.setParent(app)
def _get_abort_signals(self, owner=None):
"""Get a list of signals which should abort a question."""
abort_on = [self.shutting_down]
if owner is not None:
abort_on.append(owner.destroyed)
# This might be a generic network manager, e.g. one belonging to a
# DownloadManager. In this case, just skip the webview thing.
if self._tab_id is not None:
tab = objreg.get('tab', scope='tab', window=self._win_id,
tab=self._tab_id)
abort_on.append(tab.load_started)
return abort_on
def _ask(self, title, text, mode, owner=None, default=None):
"""Ask a blocking question in the statusbar.
@ -216,17 +211,7 @@ class NetworkManager(QNetworkAccessManager):
Return:
The answer the user gave or None if the prompt was cancelled.
"""
abort_on = [self.shutting_down]
if owner is not None:
abort_on.append(owner.destroyed)
# This might be a generic network manager, e.g. one belonging to a
# DownloadManager. In this case, just skip the webview thing.
if self._tab_id is not None:
tab = objreg.get('tab', scope='tab', window=self._win_id,
tab=self._tab_id)
abort_on.append(tab.load_started)
abort_on = self._get_abort_signals(owner)
return message.ask(title=title, text=text, mode=mode,
abort_on=abort_on, default=default)
@ -248,11 +233,8 @@ class NetworkManager(QNetworkAccessManager):
reply: The QNetworkReply that is encountering the errors.
errors: A list of errors.
"""
errors = [SslError(e) for e in errors]
ssl_strict = config.get('network', 'ssl-strict')
log.webview.debug("SSL errors {!r}, strict {}".format(
errors, ssl_strict))
errors = [webkittab.CertificateErrorWrapper(e) for e in errors]
log.webview.debug("Certificate errors {!r}".format(errors))
try:
host_tpl = urlutils.host_tuple(reply.url())
except ValueError:
@ -268,42 +250,22 @@ class NetworkManager(QNetworkAccessManager):
log.webview.debug("Already accepted: {} / "
"rejected {}".format(is_accepted, is_rejected))
if (ssl_strict and ssl_strict != 'ask') or is_rejected:
if is_rejected:
return
elif is_accepted:
reply.ignoreSslErrors()
return
if ssl_strict == 'ask':
err_template = jinja2.Template("""
Errors while loading <b>{{url.toDisplayString()}}</b>:<br/>
<ul>
{% for err in errors %}
<li>{{err.errorString()}}</li>
{% endfor %}
</ul>
""".strip())
msg = err_template.render(url=reply.url(), errors=errors)
answer = self._ask('SSL errors - continue?', msg,
mode=usertypes.PromptMode.yesno, owner=reply,
default=False)
log.webview.debug("Asked for SSL errors, answer {}".format(answer))
if answer:
reply.ignoreSslErrors()
err_dict = self._accepted_ssl_errors
else:
err_dict = self._rejected_ssl_errors
if host_tpl is not None:
err_dict[host_tpl] += errors
else:
log.webview.debug("ssl-strict is False, only warning about errors")
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('SSL error: {}'.format(err.errorString()))
abort_on = self._get_abort_signals(reply)
ignore = shared.ignore_certificate_errors(reply.url(), errors,
abort_on=abort_on)
if ignore:
reply.ignoreSslErrors()
self._accepted_ssl_errors[host_tpl] += errors
err_dict = self._accepted_ssl_errors
else:
err_dict = self._rejected_ssl_errors
if host_tpl is not None:
err_dict[host_tpl] += errors
def clear_all_ssl_errors(self):
"""Clear all remembered SSL errors."""
@ -347,12 +309,7 @@ class NetworkManager(QNetworkAccessManager):
authenticator.setUser(user)
authenticator.setPassword(password)
else:
abort_on = [self.shutting_down, reply.destroyed]
if self._tab_id is not None:
tab = objreg.get('tab', scope='tab', window=self._win_id,
tab=self._tab_id)
abort_on.append(tab.load_started)
abort_on = self._get_abort_signals(reply)
shared.authentication_required(reply.url(), authenticator,
abort_on=abort_on)

View File

@ -30,11 +30,12 @@ from PyQt5.QtWidgets import QApplication
from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
from PyQt5.QtWebKit import QWebSettings
from PyQt5.QtPrintSupport import QPrinter
from PyQt5.QtNetwork import QSslError
from qutebrowser.browser import browsertab
from qutebrowser.browser.webkit import webview, tabhistory, webkitelem
from qutebrowser.browser.webkit.network import proxy, webkitqutescheme
from qutebrowser.utils import qtutils, objreg, usertypes, utils, log
from qutebrowser.utils import qtutils, objreg, usertypes, utils, log, debug
def init():
@ -49,6 +50,36 @@ def init():
objreg.register('js-bridge', js_bridge)
class CertificateErrorWrapper(browsertab.AbstractCertificateErrorWrapper):
"""A wrapper over a QSslError."""
def __init__(self, error):
self._error = error
def __str__(self):
return self._error.errorString()
def __repr__(self):
return utils.get_repr(
self, error=debug.qenum_key(QSslError, self._error.error()),
string=str(self))
def __hash__(self):
try:
# Qt >= 5.4
return hash(self._error)
except TypeError:
return hash((self._error.certificate().toDer(),
self._error.error()))
def __eq__(self, other):
return self._error == other._error
def is_overridable(self):
return True
class WebKitPrinting(browsertab.AbstractPrinting):
"""QtWebKit implementations related to printing."""

View File

@ -430,8 +430,7 @@ def data(readonly=False):
"Whether to send DNS requests over the configured proxy."),
('ssl-strict',
SettingValue(typ.BoolAsk(), 'ask',
backends=[usertypes.Backend.QtWebKit]),
SettingValue(typ.BoolAsk(), 'ask'),
"Whether to validate SSL handshakes."),
('dns-prefetch',

View File

@ -166,16 +166,14 @@ Feature: Prompts
# SSL
@qtwebengine_todo: SSL errors are not implemented yet
Scenario: SSL error with ssl-strict = false
When I run :debug-clear-ssl-errors
And I set network -> ssl-strict to false
And I load an SSL page
And I wait until the SSL page finished loading
Then the error "SSL error: *" should be shown
Then the error "Certificate error: *" should be shown
And the page should contain the plaintext "Hello World via SSL!"
@qtwebengine_todo: SSL errors are not implemented yet
Scenario: SSL error with ssl-strict = true
When I run :debug-clear-ssl-errors
And I set network -> ssl-strict to true
@ -183,7 +181,6 @@ Feature: Prompts
Then "Error while loading *: SSL handshake failed" should be logged
And the page should contain the plaintext "Unable to load page"
@qtwebengine_todo: SSL errors are not implemented yet
Scenario: SSL error with ssl-strict = ask -> yes
When I run :debug-clear-ssl-errors
And I set network -> ssl-strict to ask
@ -193,7 +190,6 @@ Feature: Prompts
And I wait until the SSL page finished loading
Then the page should contain the plaintext "Hello World via SSL!"
@qtwebengine_todo: SSL errors are not implemented yet
Scenario: SSL error with ssl-strict = ask -> no
When I run :debug-clear-ssl-errors
And I set network -> ssl-strict to ask
@ -473,7 +469,6 @@ Feature: Prompts
# https://github.com/The-Compiler/qutebrowser/issues/1249#issuecomment-175205531
# https://github.com/The-Compiler/qutebrowser/pull/2054#issuecomment-258285544
@qtwebengine_todo: SSL errors are not implemented yet
Scenario: Interrupting SSL prompt during a notification prompt
When I set content -> notifications to ask
And I set network -> ssl-strict to ask

View File

@ -53,6 +53,19 @@ def is_ignored_qt_message(message):
return False
def is_ignored_lowlevel_message(message):
"""Check if we want to ignore a lowlevel process output."""
if 'Running without the SUID sandbox!' in message:
return True
elif message.startswith('Xlib: sequence lost'):
# https://travis-ci.org/The-Compiler/qutebrowser/jobs/157941720
# ???
return True
elif 'CERT_PKIXVerifyCert for localhost failed' in message:
return True
return False
class LogLine(testprocess.Line):
"""A parsed line from the qutebrowser log output.
@ -222,14 +235,7 @@ class QuteProc(testprocess.Process):
except testprocess.InvalidLine:
if not line.strip():
return None
elif 'Running without the SUID sandbox!' in line:
# QtWebEngine error
return None
elif line.startswith('Xlib: sequence lost'):
# https://travis-ci.org/The-Compiler/qutebrowser/jobs/157941720
# ???
return None
elif is_ignored_qt_message(line):
elif is_ignored_qt_message(line) or is_ignored_lowlevel_message(line):
return None
else:
raise