webengine: Initial SSL error support
This commit is contained in:
parent
65625a9dea
commit
6697d692e1
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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")
|
||||
|
@ -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."""
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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."""
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user