|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2013 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a SSL error handler. |
|
8 """ |
|
9 |
|
10 import contextlib |
|
11 import enum |
|
12 import platform |
|
13 |
|
14 from PyQt6.QtCore import QObject, QByteArray |
|
15 from PyQt6.QtNetwork import QSslCertificate, QSslConfiguration, QSslError, QSsl |
|
16 |
|
17 from E5Gui import E5MessageBox |
|
18 |
|
19 import Preferences |
|
20 import Utilities |
|
21 import Globals |
|
22 |
|
23 |
|
24 class EricSslErrorState(enum.Enum): |
|
25 """ |
|
26 Class defining the SSL error handling states. |
|
27 """ |
|
28 NOT_IGNORED = 0 |
|
29 SYSTEM_IGNORED = 1 |
|
30 USER_IGNORED = 2 |
|
31 |
|
32 |
|
33 class EricSslErrorHandler(QObject): |
|
34 """ |
|
35 Class implementing a handler for SSL errors. |
|
36 |
|
37 It also initializes the default SSL configuration with certificates |
|
38 permanently accepted by the user already. |
|
39 """ |
|
40 def __init__(self, parent=None): |
|
41 """ |
|
42 Constructor |
|
43 |
|
44 @param parent reference to the parent object (QObject) |
|
45 """ |
|
46 super().__init__(parent) |
|
47 |
|
48 caList = self.__getSystemCaCertificates() |
|
49 if Preferences.Prefs.settings.contains("Help/CaCertificatesDict"): |
|
50 # port old entries stored under 'Help' |
|
51 certificateDict = Globals.toDict( |
|
52 Preferences.Prefs.settings.value("Help/CaCertificatesDict")) |
|
53 Preferences.Prefs.settings.setValue( |
|
54 "Ssl/CaCertificatesDict", certificateDict) |
|
55 Preferences.Prefs.settings.remove("Help/CaCertificatesDict") |
|
56 else: |
|
57 certificateDict = Globals.toDict( |
|
58 Preferences.Prefs.settings.value("Ssl/CaCertificatesDict")) |
|
59 for server in certificateDict: |
|
60 for cert in QSslCertificate.fromData(certificateDict[server]): |
|
61 if cert not in caList: |
|
62 caList.append(cert) |
|
63 sslCfg = QSslConfiguration.defaultConfiguration() |
|
64 sslCfg.setCaCertificates(caList) |
|
65 try: |
|
66 sslProtocol = QSsl.SslProtocol.TlsV1_1OrLater |
|
67 if Globals.isWindowsPlatform() and platform.win32_ver()[0] == '7': |
|
68 sslProtocol = QSsl.SslProtocol.SecureProtocols |
|
69 except AttributeError: |
|
70 sslProtocol = QSsl.SslProtocol.SecureProtocols |
|
71 sslCfg.setProtocol(sslProtocol) |
|
72 with contextlib.suppress(AttributeError): |
|
73 sslCfg.setSslOption(QSsl.SslOption.SslOptionDisableCompression, |
|
74 True) |
|
75 QSslConfiguration.setDefaultConfiguration(sslCfg) |
|
76 |
|
77 def sslErrorsReplySlot(self, reply, errors): |
|
78 """ |
|
79 Public slot to handle SSL errors for a network reply. |
|
80 |
|
81 @param reply reference to the reply object (QNetworkReply) |
|
82 @param errors list of SSL errors (list of QSslError) |
|
83 """ |
|
84 self.sslErrorsReply(reply, errors) |
|
85 |
|
86 def sslErrorsReply(self, reply, errors): |
|
87 """ |
|
88 Public slot to handle SSL errors for a network reply. |
|
89 |
|
90 @param reply reference to the reply object (QNetworkReply) |
|
91 @param errors list of SSL errors (list of QSslError) |
|
92 @return tuple indicating to ignore the SSL errors (one of NotIgnored, |
|
93 SystemIgnored or UserIgnored) and indicating a change of the |
|
94 default SSL configuration (boolean) |
|
95 """ |
|
96 url = reply.url() |
|
97 ignore, defaultChanged = self.sslErrors(errors, url.host(), url.port()) |
|
98 if ignore: |
|
99 if defaultChanged: |
|
100 reply.setSslConfiguration( |
|
101 QSslConfiguration.defaultConfiguration()) |
|
102 reply.ignoreSslErrors() |
|
103 else: |
|
104 reply.abort() |
|
105 |
|
106 return ignore, defaultChanged |
|
107 |
|
108 def sslErrors(self, errors, server, port=-1): |
|
109 """ |
|
110 Public method to handle SSL errors. |
|
111 |
|
112 @param errors list of SSL errors |
|
113 @type list of QSslError |
|
114 @param server name of the server |
|
115 @type str |
|
116 @param port value of the port |
|
117 @type int |
|
118 @return tuple indicating to ignore the SSL errors and indicating a |
|
119 change of the default SSL configuration |
|
120 @rtype tuple of (EricSslErrorState, bool) |
|
121 """ |
|
122 caMerge = {} |
|
123 certificateDict = Globals.toDict( |
|
124 Preferences.Prefs.settings.value("Ssl/CaCertificatesDict")) |
|
125 for caServer in certificateDict: |
|
126 caMerge[caServer] = QSslCertificate.fromData( |
|
127 certificateDict[caServer]) |
|
128 caNew = [] |
|
129 |
|
130 errorStrings = [] |
|
131 if port != -1: |
|
132 server += ":{0:d}".format(port) |
|
133 if errors: |
|
134 for err in errors: |
|
135 if err.error() == QSslError.SslError.NoError: |
|
136 continue |
|
137 if server in caMerge and err.certificate() in caMerge[server]: |
|
138 continue |
|
139 errorStrings.append(err.errorString()) |
|
140 if not err.certificate().isNull(): |
|
141 cert = err.certificate() |
|
142 if cert not in caNew: |
|
143 caNew.append(cert) |
|
144 if not errorStrings: |
|
145 return EricSslErrorState.SYSTEM_IGNORED, False |
|
146 |
|
147 errorString = '.</li><li>'.join(errorStrings) |
|
148 ret = E5MessageBox.yesNo( |
|
149 None, |
|
150 self.tr("SSL Errors"), |
|
151 self.tr("""<p>SSL Errors for <br /><b>{0}</b>""" |
|
152 """<ul><li>{1}</li></ul></p>""" |
|
153 """<p>Do you want to ignore these errors?</p>""") |
|
154 .format(server, errorString), |
|
155 icon=E5MessageBox.Warning) |
|
156 |
|
157 if ret: |
|
158 caRet = False |
|
159 if len(caNew) > 0: |
|
160 certinfos = [] |
|
161 for cert in caNew: |
|
162 certinfos.append(self.__certToString(cert)) |
|
163 caRet = E5MessageBox.yesNo( |
|
164 None, |
|
165 self.tr("Certificates"), |
|
166 self.tr( |
|
167 """<p>Certificates:<br/>{0}<br/>""" |
|
168 """Do you want to accept all these certificates?""" |
|
169 """</p>""") |
|
170 .format("".join(certinfos))) |
|
171 if caRet: |
|
172 if server not in caMerge: |
|
173 caMerge[server] = [] |
|
174 for cert in caNew: |
|
175 caMerge[server].append(cert) |
|
176 |
|
177 sslCfg = QSslConfiguration.defaultConfiguration() |
|
178 caList = sslCfg.caCertificates() |
|
179 for cert in caNew: |
|
180 caList.append(cert) |
|
181 sslCfg.setCaCertificates(caList) |
|
182 try: |
|
183 sslCfg.setProtocol(QSsl.SslProtocol.TlsV1_1OrLater) |
|
184 except AttributeError: |
|
185 sslCfg.setProtocol(QSsl.SslProtocol.SecureProtocols) |
|
186 with contextlib.suppress(AttributeError): |
|
187 sslCfg.setSslOption( |
|
188 QSsl.SslOption.SslOptionDisableCompression, |
|
189 True) |
|
190 QSslConfiguration.setDefaultConfiguration(sslCfg) |
|
191 |
|
192 certificateDict = {} |
|
193 for server in caMerge: |
|
194 pems = QByteArray() |
|
195 for cert in caMerge[server]: |
|
196 pems.append(cert.toPem() + b'\n') |
|
197 certificateDict[server] = pems |
|
198 Preferences.Prefs.settings.setValue( |
|
199 "Ssl/CaCertificatesDict", |
|
200 certificateDict) |
|
201 |
|
202 return EricSslErrorState.USER_IGNORED, caRet |
|
203 |
|
204 else: |
|
205 return EricSslErrorState.NOT_IGNORED, False |
|
206 |
|
207 def __certToString(self, cert): |
|
208 """ |
|
209 Private method to convert a certificate to a formatted string. |
|
210 |
|
211 @param cert certificate to convert (QSslCertificate) |
|
212 @return formatted string (string) |
|
213 """ |
|
214 result = "<p>" |
|
215 |
|
216 result += self.tr( |
|
217 "Name: {0}" |
|
218 ).format( |
|
219 Utilities.html_encode( |
|
220 Utilities.decodeString( |
|
221 ", ".join(cert.subjectInfo( |
|
222 QSslCertificate.SubjectInfo.CommonName)) |
|
223 ) |
|
224 ) |
|
225 ) |
|
226 |
|
227 result += self.tr( |
|
228 "<br/>Organization: {0}" |
|
229 ).format( |
|
230 Utilities.html_encode( |
|
231 Utilities.decodeString( |
|
232 ", ".join(cert.subjectInfo( |
|
233 QSslCertificate.SubjectInfo.Organization)) |
|
234 ) |
|
235 ) |
|
236 ) |
|
237 |
|
238 result += self.tr( |
|
239 "<br/>Issuer: {0}" |
|
240 ).format( |
|
241 Utilities.html_encode( |
|
242 Utilities.decodeString( |
|
243 ", ".join(cert.issuerInfo( |
|
244 QSslCertificate.SubjectInfo.CommonName)) |
|
245 ) |
|
246 ) |
|
247 ) |
|
248 result += self.tr( |
|
249 "<br/>Not valid before: {0}<br/>Valid Until: {1}" |
|
250 ).format( |
|
251 Utilities.html_encode( |
|
252 cert.effectiveDate().toString("yyyy-MM-dd") |
|
253 ), |
|
254 Utilities.html_encode( |
|
255 cert.expiryDate().toString("yyyy-MM-dd") |
|
256 ) |
|
257 ) |
|
258 |
|
259 result += "</p>" |
|
260 |
|
261 return result |
|
262 |
|
263 def __getSystemCaCertificates(self): |
|
264 """ |
|
265 Private method to get the list of system certificates. |
|
266 |
|
267 @return list of system certificates (list of QSslCertificate) |
|
268 """ |
|
269 caList = QSslCertificate.fromData(Globals.toByteArray( |
|
270 Preferences.Prefs.settings.value("Ssl/SystemCertificates"))) |
|
271 if not caList: |
|
272 caList = QSslConfiguration.systemCaCertificates() |
|
273 return caList |