eric7/WebBrowser/Network/NetworkManager.py

branch
eric7
changeset 8312
800c432b34c8
parent 8260
2161475d9639
child 8318
962bce857696
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2016 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a network manager class.
8 """
9
10 import json
11 import contextlib
12
13 from PyQt5.QtCore import pyqtSignal, QByteArray
14 from PyQt5.QtWidgets import QStyle, QDialog
15 from PyQt5.QtNetwork import (
16 QNetworkAccessManager, QNetworkProxy, QNetworkProxyFactory, QNetworkRequest
17 )
18
19 from E5Gui import E5MessageBox
20 from E5Gui.E5Application import e5App
21
22 from E5Network.E5NetworkProxyFactory import proxyAuthenticationRequired
23 try:
24 from E5Network.E5SslErrorHandler import E5SslErrorHandler
25 SSL_AVAILABLE = True
26 except ImportError:
27 SSL_AVAILABLE = False
28
29 from WebBrowser.WebBrowserWindow import WebBrowserWindow
30 from .NetworkUrlInterceptor import NetworkUrlInterceptor
31 from ..Tools.WebBrowserTools import getHtmlPage, pixmapToDataUrl
32
33 from Utilities.AutoSaver import AutoSaver
34 import Preferences
35
36
37 class NetworkManager(QNetworkAccessManager):
38 """
39 Class implementing a network manager.
40
41 @signal changed() emitted to indicate a change
42 """
43 changed = pyqtSignal()
44
45 def __init__(self, engine, parent=None):
46 """
47 Constructor
48
49 @param engine reference to the help engine (QHelpEngine)
50 @param parent reference to the parent object (QObject)
51 """
52 super().__init__(parent)
53
54 from E5Network.E5NetworkProxyFactory import E5NetworkProxyFactory
55
56 self.__proxyFactory = E5NetworkProxyFactory()
57 if Preferences.getUI("UseSystemProxy"):
58 QNetworkProxyFactory.setUseSystemConfiguration(True)
59 else:
60 QNetworkProxyFactory.setApplicationProxyFactory(
61 self.__proxyFactory)
62 QNetworkProxyFactory.setUseSystemConfiguration(False)
63
64 self.languagesChanged()
65
66 if SSL_AVAILABLE:
67 self.__sslErrorHandler = E5SslErrorHandler(self)
68 self.sslErrors.connect(self.__sslErrorHandlingSlot)
69
70 self.__temporarilyIgnoredSslErrors = {}
71 self.__permanentlyIgnoredSslErrors = {}
72 # dictionaries of permanently and temporarily ignored SSL errors
73
74 self.__insecureHosts = set()
75
76 self.__loaded = False
77 self.__saveTimer = AutoSaver(self, self.__save)
78
79 self.changed.connect(self.__saveTimer.changeOccurred)
80 self.proxyAuthenticationRequired.connect(proxyAuthenticationRequired)
81 self.authenticationRequired.connect(
82 lambda reply, auth: self.authentication(reply.url(), auth))
83
84 from .EricSchemeHandler import EricSchemeHandler
85 self.__ericSchemeHandler = EricSchemeHandler()
86 WebBrowserWindow.webProfile().installUrlSchemeHandler(
87 QByteArray(b"eric"), self.__ericSchemeHandler)
88
89 if engine:
90 from .QtHelpSchemeHandler import QtHelpSchemeHandler
91 self.__qtHelpSchemeHandler = QtHelpSchemeHandler(engine)
92 WebBrowserWindow.webProfile().installUrlSchemeHandler(
93 QByteArray(b"qthelp"), self.__qtHelpSchemeHandler)
94
95 self.__interceptor = NetworkUrlInterceptor(self)
96 try:
97 WebBrowserWindow.webProfile().setUrlRequestInterceptor(
98 self.__interceptor)
99 except AttributeError:
100 # Qt < 5.13
101 WebBrowserWindow.webProfile().setRequestInterceptor(
102 self.__interceptor)
103
104 WebBrowserWindow.cookieJar()
105
106 def __save(self):
107 """
108 Private slot to save the permanent SSL error exceptions.
109 """
110 if not self.__loaded:
111 return
112
113 from WebBrowser.WebBrowserWindow import WebBrowserWindow
114 if not WebBrowserWindow.isPrivate():
115 dbString = json.dumps(self.__permanentlyIgnoredSslErrors)
116 Preferences.setWebBrowser("SslExceptionsDB", dbString)
117
118 def __load(self):
119 """
120 Private method to load the permanent SSL error exceptions.
121 """
122 if self.__loaded:
123 return
124
125 dbString = Preferences.getWebBrowser("SslExceptionsDB")
126 if dbString:
127 with contextlib.suppress(ValueError):
128 db = json.loads(dbString)
129 self.__permanentlyIgnoredSslErrors = db
130
131 self.__loaded = True
132
133 def shutdown(self):
134 """
135 Public method to shut down the network manager.
136 """
137 self.__saveTimer.saveIfNeccessary()
138 self.__loaded = False
139 self.__temporarilyIgnoredSslErrors = {}
140 self.__permanentlyIgnoredSslErrors = {}
141
142 # set proxy factory to None to avoid crashes
143 QNetworkProxyFactory.setApplicationProxyFactory(None)
144
145 def showSslErrorExceptionsDialog(self):
146 """
147 Public method to show the SSL error exceptions dialog.
148 """
149 self.__load()
150
151 from .SslErrorExceptionsDialog import SslErrorExceptionsDialog
152 dlg = SslErrorExceptionsDialog(self.__permanentlyIgnoredSslErrors)
153 if dlg.exec() == QDialog.DialogCode.Accepted:
154 self.__permanentlyIgnoredSslErrors = dlg.getSslErrorExceptions()
155 self.changed.emit()
156
157 def clearSslExceptions(self):
158 """
159 Public method to clear the permanent SSL certificate error exceptions.
160 """
161 self.__load()
162
163 self.__permanentlyIgnoredSslErrors = {}
164 self.changed.emit()
165 self.__saveTimer.saveIfNeccessary()
166
167 def certificateError(self, error, view):
168 """
169 Public method to handle SSL certificate errors.
170
171 @param error object containing the certificate error information
172 @type QWebEngineCertificateError
173 @param view reference to a view to be used as parent for the dialog
174 @type QWidget
175 @return flag indicating to ignore this error
176 @rtype bool
177 """
178 if Preferences.getWebBrowser("AlwaysRejectFaultyCertificates"):
179 return False
180
181 self.__load()
182
183 host = error.url().host()
184
185 self.__insecureHosts.add(host)
186
187 if (
188 host in self.__temporarilyIgnoredSslErrors and
189 error.error() in self.__temporarilyIgnoredSslErrors[host]
190 ):
191 return True
192
193 if (
194 host in self.__permanentlyIgnoredSslErrors and
195 error.error() in self.__permanentlyIgnoredSslErrors[host]
196 ):
197 return True
198
199 title = self.tr("SSL Certificate Error")
200 msgBox = E5MessageBox.E5MessageBox(
201 E5MessageBox.Warning,
202 title,
203 self.tr("""<b>{0}</b>"""
204 """<p>The host <b>{1}</b> you are trying to access has"""
205 """ errors in the SSL certificate.</p>"""
206 """<ul><li>{2}</li></ul>"""
207 """<p>Would you like to make an exception?</p>""")
208 .format(title, host, error.errorDescription()),
209 modal=True, parent=view)
210 permButton = msgBox.addButton(self.tr("&Permanent accept"),
211 E5MessageBox.AcceptRole)
212 tempButton = msgBox.addButton(self.tr("&Temporary accept"),
213 E5MessageBox.AcceptRole)
214 msgBox.addButton(self.tr("&Reject"), E5MessageBox.RejectRole)
215 msgBox.exec()
216 if msgBox.clickedButton() == permButton:
217 if host not in self.__permanentlyIgnoredSslErrors:
218 self.__permanentlyIgnoredSslErrors[host] = []
219 self.__permanentlyIgnoredSslErrors[host].append(error.error())
220 self.changed.emit()
221 return True
222 elif msgBox.clickedButton() == tempButton:
223 if host not in self.__temporarilyIgnoredSslErrors:
224 self.__temporarilyIgnoredSslErrors[host] = []
225 self.__temporarilyIgnoredSslErrors[host].append(error.error())
226 return True
227 else:
228 return False
229
230 def __sslErrorHandlingSlot(self, reply, errors):
231 """
232 Private slot to handle SSL errors for a network reply.
233
234 @param reply reference to the reply object
235 @type QNetworkReply
236 @param errors list of SSL errors
237 @type list of QSslError
238 """
239 if Preferences.getWebBrowser("AlwaysRejectFaultyCertificates"):
240 return
241
242 self.__load()
243
244 host = reply.url().host()
245 if (
246 host in self.__permanentlyIgnoredSslErrors or
247 host in self.__temporarilyIgnoredSslErrors
248 ):
249 reply.ignoreSslErrors()
250 else:
251 self.__sslErrorHandler.sslErrorsReply(reply, errors)
252
253 def isInsecureHost(self, host):
254 """
255 Public method to check a host against the list of insecure hosts.
256
257 @param host name of the host to be checked
258 @type str
259 @return flag indicating an insecure host
260 @rtype bool
261 """
262 return host in self.__insecureHosts
263
264 def authentication(self, url, auth, page=None):
265 """
266 Public slot to handle an authentication request.
267
268 @param url URL requesting authentication
269 @type QUrl
270 @param auth reference to the authenticator object
271 @type QAuthenticator
272 @param page reference to the web page
273 @type QWebEnginePage or None
274 """
275 urlRoot = (
276 "{0}://{1}".format(url.scheme(), url.authority())
277 )
278 realm = auth.realm()
279 if not realm and 'realm' in auth.options():
280 realm = auth.option("realm")
281 info = (
282 self.tr("<b>Enter username and password for '{0}', realm '{1}'</b>"
283 ).format(urlRoot, realm)
284 if realm else
285 self.tr("<b>Enter username and password for '{0}'</b>"
286 ).format(urlRoot)
287 )
288
289 from UI.AuthenticationDialog import AuthenticationDialog
290 import WebBrowser.WebBrowserWindow
291
292 dlg = AuthenticationDialog(info, auth.user(),
293 Preferences.getUser("SavePasswords"),
294 Preferences.getUser("SavePasswords"))
295 if Preferences.getUser("SavePasswords"):
296 username, password = (
297 WebBrowser.WebBrowserWindow.WebBrowserWindow
298 .passwordManager().getLogin(url, realm)
299 )
300 if username:
301 dlg.setData(username, password)
302 if dlg.exec() == QDialog.DialogCode.Accepted:
303 username, password = dlg.getData()
304 auth.setUser(username)
305 auth.setPassword(password)
306 if Preferences.getUser("SavePasswords") and dlg.shallSave():
307 (
308 WebBrowser.WebBrowserWindow.WebBrowserWindow
309 .passwordManager().setLogin(
310 url, realm, username, password)
311 )
312 else:
313 if page is not None:
314 self.__showAuthenticationErrorPage(page, url)
315
316 def __showAuthenticationErrorPage(self, page, url):
317 """
318 Private method to show an authentication error page.
319
320 @param page reference to the page
321 @type QWebEnginePage
322 @param url reference to the URL requesting authentication
323 @type QUrl
324 """
325 html = getHtmlPage("authenticationErrorPage.html")
326 html = html.replace("@IMAGE@", pixmapToDataUrl(
327 e5App().style().standardIcon(
328 QStyle.StandardPixmap.SP_MessageBoxCritical).pixmap(48, 48)
329 ).toString())
330 html = html.replace("@FAVICON@", pixmapToDataUrl(
331 e5App().style() .standardIcon(
332 QStyle.StandardPixmap.SP_MessageBoxCritical).pixmap(16, 16)
333 ).toString())
334 html = html.replace("@TITLE@", self.tr("Authentication required"))
335 html = html.replace("@H1@", self.tr("Authentication required"))
336 html = html.replace(
337 "@LI-1@",
338 self.tr("Authentication is required to access:"))
339 html = html.replace(
340 "@LI-2@",
341 '<a href="{0}">{0}</a>'.format(url.toString()))
342 page.setHtml(html, url)
343
344 def proxyAuthentication(self, requestUrl, auth, proxyHost):
345 """
346 Public slot to handle a proxy authentication request.
347
348 @param requestUrl requested URL
349 @type QUrl
350 @param auth reference to the authenticator object
351 @type QAuthenticator
352 @param proxyHost name of the proxy host
353 @type str
354 """
355 proxy = QNetworkProxy.applicationProxy()
356 if proxy.user() and proxy.password():
357 auth.setUser(proxy.user())
358 auth.setPassword(proxy.password())
359 return
360
361 proxyAuthenticationRequired(proxy, auth)
362
363 def languagesChanged(self):
364 """
365 Public slot to (re-)load the list of accepted languages.
366 """
367 from WebBrowser.WebBrowserLanguagesDialog import (
368 WebBrowserLanguagesDialog
369 )
370 languages = Preferences.toList(
371 Preferences.Prefs.settings.value(
372 "WebBrowser/AcceptLanguages",
373 WebBrowserLanguagesDialog.defaultAcceptLanguages()))
374 self.__acceptLanguage = WebBrowserLanguagesDialog.httpString(languages)
375
376 WebBrowserWindow.webProfile().setHttpAcceptLanguage(
377 self.__acceptLanguage)
378
379 def installUrlInterceptor(self, interceptor):
380 """
381 Public method to install an URL interceptor.
382
383 @param interceptor URL interceptor to be installed
384 @type UrlInterceptor
385 """
386 self.__interceptor.installUrlInterceptor(interceptor)
387
388 def removeUrlInterceptor(self, interceptor):
389 """
390 Public method to remove an URL interceptor.
391
392 @param interceptor URL interceptor to be removed
393 @type UrlInterceptor
394 """
395 self.__interceptor.removeUrlInterceptor(interceptor)
396
397 def preferencesChanged(self):
398 """
399 Public slot to handle a change of preferences.
400 """
401 self.__interceptor.preferencesChanged()
402
403 if Preferences.getUI("UseSystemProxy"):
404 QNetworkProxyFactory.setUseSystemConfiguration(True)
405 else:
406 QNetworkProxyFactory.setApplicationProxyFactory(
407 self.__proxyFactory)
408 QNetworkProxyFactory.setUseSystemConfiguration(False)
409
410 def createRequest(self, op, request, data):
411 """
412 Public method to launch a network action.
413
414 @param op operation to be performed
415 @type QNetworkAccessManager.Operation
416 @param request request to be operated on
417 @type QNetworkRequest
418 @param data reference to the data to be sent
419 @type QIODevice
420 @return reference to the network reply
421 @rtype QNetworkReply
422 """
423 req = QNetworkRequest(request)
424 req.setAttribute(
425 QNetworkRequest.Attribute.SpdyAllowedAttribute, True)
426 req.setAttribute(
427 QNetworkRequest.Attribute.FollowRedirectsAttribute, True)
428
429 return super().createRequest(op, req, data)

eric ide

mercurial