src/eric7/WebBrowser/Network/NetworkManager.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8881
54e42bc2437a
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2016 - 2022 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 PyQt6.QtCore import pyqtSignal, QByteArray
14 from PyQt6.QtWidgets import QStyle, QDialog
15 from PyQt6.QtNetwork import (
16 QNetworkAccessManager, QNetworkProxy, QNetworkProxyFactory, QNetworkRequest
17 )
18
19 from EricWidgets import EricMessageBox
20 from EricWidgets.EricApplication import ericApp
21
22 from EricNetwork.EricNetworkProxyFactory import proxyAuthenticationRequired
23 try:
24 from EricNetwork.EricSslErrorHandler import EricSslErrorHandler
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 EricNetwork.EricNetworkProxyFactory import EricNetworkProxyFactory
55
56 self.__proxyFactory = EricNetworkProxyFactory()
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 = EricSslErrorHandler(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 WebBrowserWindow.webProfile().setUrlRequestInterceptor(
97 self.__interceptor)
98
99 WebBrowserWindow.cookieJar()
100
101 def __save(self):
102 """
103 Private slot to save the permanent SSL error exceptions.
104 """
105 if not self.__loaded:
106 return
107
108 from WebBrowser.WebBrowserWindow import WebBrowserWindow
109 if not WebBrowserWindow.isPrivate():
110 dbString = json.dumps(self.__permanentlyIgnoredSslErrors)
111 Preferences.setWebBrowser("SslExceptionsDB", dbString)
112
113 def __load(self):
114 """
115 Private method to load the permanent SSL error exceptions.
116 """
117 if self.__loaded:
118 return
119
120 dbString = Preferences.getWebBrowser("SslExceptionsDB")
121 if dbString:
122 with contextlib.suppress(ValueError):
123 db = json.loads(dbString)
124 self.__permanentlyIgnoredSslErrors = db
125
126 self.__loaded = True
127
128 def shutdown(self):
129 """
130 Public method to shut down the network manager.
131 """
132 self.__saveTimer.saveIfNeccessary()
133 self.__loaded = False
134 self.__temporarilyIgnoredSslErrors = {}
135 self.__permanentlyIgnoredSslErrors = {}
136
137 # set proxy factory to None to avoid crashes
138 QNetworkProxyFactory.setApplicationProxyFactory(None)
139
140 def showSslErrorExceptionsDialog(self):
141 """
142 Public method to show the SSL error exceptions dialog.
143 """
144 self.__load()
145
146 from .SslErrorExceptionsDialog import SslErrorExceptionsDialog
147 dlg = SslErrorExceptionsDialog(self.__permanentlyIgnoredSslErrors)
148 if dlg.exec() == QDialog.DialogCode.Accepted:
149 self.__permanentlyIgnoredSslErrors = dlg.getSslErrorExceptions()
150 self.changed.emit()
151
152 def clearSslExceptions(self):
153 """
154 Public method to clear the permanent SSL certificate error exceptions.
155 """
156 self.__load()
157
158 self.__permanentlyIgnoredSslErrors = {}
159 self.changed.emit()
160 self.__saveTimer.saveIfNeccessary()
161
162 def certificateError(self, error, view):
163 """
164 Public method to handle SSL certificate errors.
165
166 @param error object containing the certificate error information
167 @type QWebEngineCertificateError
168 @param view reference to a view to be used as parent for the dialog
169 @type QWidget
170 @return flag indicating to ignore this error
171 @rtype bool
172 """
173 if Preferences.getWebBrowser("AlwaysRejectFaultyCertificates"):
174 return False
175
176 self.__load()
177
178 host = error.url().host()
179
180 self.__insecureHosts.add(host)
181
182 if (
183 host in self.__temporarilyIgnoredSslErrors and
184 error.error() in self.__temporarilyIgnoredSslErrors[host]
185 ):
186 return True
187
188 if (
189 host in self.__permanentlyIgnoredSslErrors and
190 error.error() in self.__permanentlyIgnoredSslErrors[host]
191 ):
192 return True
193
194 title = self.tr("SSL Certificate Error")
195 msgBox = EricMessageBox.EricMessageBox(
196 EricMessageBox.Warning,
197 title,
198 self.tr("""<b>{0}</b>"""
199 """<p>The host <b>{1}</b> you are trying to access has"""
200 """ errors in the SSL certificate.</p>"""
201 """<ul><li>{2}</li></ul>"""
202 """<p>Would you like to make an exception?</p>""")
203 .format(title, host, error.errorDescription()),
204 modal=True, parent=view)
205 permButton = msgBox.addButton(self.tr("&Permanent accept"),
206 EricMessageBox.AcceptRole)
207 tempButton = msgBox.addButton(self.tr("&Temporary accept"),
208 EricMessageBox.AcceptRole)
209 msgBox.addButton(self.tr("&Reject"), EricMessageBox.RejectRole)
210 msgBox.exec()
211 if msgBox.clickedButton() == permButton:
212 if host not in self.__permanentlyIgnoredSslErrors:
213 self.__permanentlyIgnoredSslErrors[host] = []
214 self.__permanentlyIgnoredSslErrors[host].append(error.error())
215 self.changed.emit()
216 return True
217 elif msgBox.clickedButton() == tempButton:
218 if host not in self.__temporarilyIgnoredSslErrors:
219 self.__temporarilyIgnoredSslErrors[host] = []
220 self.__temporarilyIgnoredSslErrors[host].append(error.error())
221 return True
222 else:
223 return False
224
225 def __sslErrorHandlingSlot(self, reply, errors):
226 """
227 Private slot to handle SSL errors for a network reply.
228
229 @param reply reference to the reply object
230 @type QNetworkReply
231 @param errors list of SSL errors
232 @type list of QSslError
233 """
234 if Preferences.getWebBrowser("AlwaysRejectFaultyCertificates"):
235 return
236
237 self.__load()
238
239 host = reply.url().host()
240 if (
241 host in self.__permanentlyIgnoredSslErrors or
242 host in self.__temporarilyIgnoredSslErrors
243 ):
244 reply.ignoreSslErrors()
245 else:
246 self.__sslErrorHandler.sslErrorsReply(reply, errors)
247
248 def isInsecureHost(self, host):
249 """
250 Public method to check a host against the list of insecure hosts.
251
252 @param host name of the host to be checked
253 @type str
254 @return flag indicating an insecure host
255 @rtype bool
256 """
257 return host in self.__insecureHosts
258
259 def authentication(self, url, auth, page=None):
260 """
261 Public slot to handle an authentication request.
262
263 @param url URL requesting authentication
264 @type QUrl
265 @param auth reference to the authenticator object
266 @type QAuthenticator
267 @param page reference to the web page
268 @type QWebEnginePage or None
269 """
270 urlRoot = (
271 "{0}://{1}".format(url.scheme(), url.authority())
272 )
273 realm = auth.realm()
274 if not realm and 'realm' in auth.options():
275 realm = auth.option("realm")
276 info = (
277 self.tr("<b>Enter username and password for '{0}', realm '{1}'</b>"
278 ).format(urlRoot, realm)
279 if realm else
280 self.tr("<b>Enter username and password for '{0}'</b>"
281 ).format(urlRoot)
282 )
283
284 from UI.AuthenticationDialog import AuthenticationDialog
285 import WebBrowser.WebBrowserWindow
286
287 dlg = AuthenticationDialog(info, auth.user(),
288 Preferences.getUser("SavePasswords"),
289 Preferences.getUser("SavePasswords"))
290 if Preferences.getUser("SavePasswords"):
291 username, password = (
292 WebBrowser.WebBrowserWindow.WebBrowserWindow
293 .passwordManager().getLogin(url, realm)
294 )
295 if username:
296 dlg.setData(username, password)
297 if dlg.exec() == QDialog.DialogCode.Accepted:
298 username, password = dlg.getData()
299 auth.setUser(username)
300 auth.setPassword(password)
301 if Preferences.getUser("SavePasswords") and dlg.shallSave():
302 (
303 WebBrowser.WebBrowserWindow.WebBrowserWindow
304 .passwordManager().setLogin(
305 url, realm, username, password)
306 )
307 else:
308 if page is not None:
309 self.__showAuthenticationErrorPage(page, url)
310
311 def __showAuthenticationErrorPage(self, page, url):
312 """
313 Private method to show an authentication error page.
314
315 @param page reference to the page
316 @type QWebEnginePage
317 @param url reference to the URL requesting authentication
318 @type QUrl
319 """
320 html = getHtmlPage("authenticationErrorPage.html")
321 html = html.replace("@IMAGE@", pixmapToDataUrl(
322 ericApp().style().standardIcon(
323 QStyle.StandardPixmap.SP_MessageBoxCritical).pixmap(48, 48)
324 ).toString())
325 html = html.replace("@FAVICON@", pixmapToDataUrl(
326 ericApp().style() .standardIcon(
327 QStyle.StandardPixmap.SP_MessageBoxCritical).pixmap(16, 16)
328 ).toString())
329 html = html.replace("@TITLE@", self.tr("Authentication required"))
330 html = html.replace("@H1@", self.tr("Authentication required"))
331 html = html.replace(
332 "@LI-1@",
333 self.tr("Authentication is required to access:"))
334 html = html.replace(
335 "@LI-2@",
336 '<a href="{0}">{0}</a>'.format(url.toString()))
337 page.setHtml(html, url)
338
339 def proxyAuthentication(self, requestUrl, auth, proxyHost):
340 """
341 Public slot to handle a proxy authentication request.
342
343 @param requestUrl requested URL
344 @type QUrl
345 @param auth reference to the authenticator object
346 @type QAuthenticator
347 @param proxyHost name of the proxy host
348 @type str
349 """
350 proxy = QNetworkProxy.applicationProxy()
351 if proxy.user() and proxy.password():
352 auth.setUser(proxy.user())
353 auth.setPassword(proxy.password())
354 return
355
356 proxyAuthenticationRequired(proxy, auth)
357
358 def languagesChanged(self):
359 """
360 Public slot to (re-)load the list of accepted languages.
361 """
362 from WebBrowser.WebBrowserLanguagesDialog import (
363 WebBrowserLanguagesDialog
364 )
365 languages = Preferences.toList(
366 Preferences.getSettings().value(
367 "WebBrowser/AcceptLanguages",
368 WebBrowserLanguagesDialog.defaultAcceptLanguages()))
369 self.__acceptLanguage = WebBrowserLanguagesDialog.httpString(languages)
370
371 WebBrowserWindow.webProfile().setHttpAcceptLanguage(
372 self.__acceptLanguage)
373
374 def installUrlInterceptor(self, interceptor):
375 """
376 Public method to install an URL interceptor.
377
378 @param interceptor URL interceptor to be installed
379 @type UrlInterceptor
380 """
381 self.__interceptor.installUrlInterceptor(interceptor)
382
383 def removeUrlInterceptor(self, interceptor):
384 """
385 Public method to remove an URL interceptor.
386
387 @param interceptor URL interceptor to be removed
388 @type UrlInterceptor
389 """
390 self.__interceptor.removeUrlInterceptor(interceptor)
391
392 def preferencesChanged(self):
393 """
394 Public slot to handle a change of preferences.
395 """
396 self.__interceptor.preferencesChanged()
397
398 if Preferences.getUI("UseSystemProxy"):
399 QNetworkProxyFactory.setUseSystemConfiguration(True)
400 else:
401 QNetworkProxyFactory.setApplicationProxyFactory(
402 self.__proxyFactory)
403 QNetworkProxyFactory.setUseSystemConfiguration(False)
404
405 def createRequest(self, op, request, data):
406 """
407 Public method to launch a network action.
408
409 @param op operation to be performed
410 @type QNetworkAccessManager.Operation
411 @param request request to be operated on
412 @type QNetworkRequest
413 @param data reference to the data to be sent
414 @type QIODevice
415 @return reference to the network reply
416 @rtype QNetworkReply
417 """
418 req = QNetworkRequest(request)
419 req.setAttribute(
420 QNetworkRequest.Attribute.Http2AllowedAttribute, True)
421
422 return super().createRequest(op, req, data)

eric ide

mercurial