|
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) |