11# A part of NonVisual Desktop Access (NVDA)
2- # Copyright (C) 2022-2024 NV Access Limited
2+ # Copyright (C) 2022-2025 NV Access Limited
33# This file is covered by the GNU General Public License.
44# See the file COPYING for more details.
55
66from concurrent .futures import (
77 Future ,
88 ThreadPoolExecutor ,
99)
10+ import hashlib
1011import os
1112import pathlib
1213import shutil
1819 Optional ,
1920 Tuple ,
2021)
22+ from urllib .parse import urlparse
2123
2224import requests
2325
2830from NVDAState import WritePaths
2931import threading
3032from utils .security import sha256_checksum
33+ from utils .networking import (
34+ _getCertificate ,
35+ _is_cert_verification_error ,
36+ _updateWindowsRootCertificates ,
37+ )
3138from config import conf
3239
3340from .models .addon import (
@@ -216,7 +223,11 @@ def _downloadAddonToPath(
216223 # Some add-ons are quite large, so we need to allow for a long download time.
217224 # 1GB at 0.5 MB/s takes 4.5hr to download.
218225 MAX_ADDON_DOWNLOAD_TIME = 60 * 60 * 6 # 6 hours
219- with requests .get (addonData .model .URL , stream = True , timeout = MAX_ADDON_DOWNLOAD_TIME ) as r :
226+ with requests .get (
227+ addonData .model .URL ,
228+ stream = True ,
229+ timeout = MAX_ADDON_DOWNLOAD_TIME ,
230+ ) as r :
220231 with open (downloadFilePath , "wb" ) as fd :
221232 # Most add-ons are small. This value was chosen quite arbitrarily, but with the intention to allow
222233 # interrupting the download. This is particularly important on a slow connection, to provide
@@ -236,11 +247,56 @@ def _downloadAddonToPath(
236247 return False # The download was cancelled
237248 return True
238249
239- def _download ( self , listItem : "AddonListItemVM[_AddonStoreModel]" ) -> Optional [ os . PathLike ]:
240- from gui . message import DisplayableError
250+ # Translators: A title for a dialog notifying a user of an add-on download failure.
251+ _addonDownloadFailureMessageTitle = pgettext ( "addonStore" , "Add-on download failure" )
241252
242- # Translators: A title for a dialog notifying a user of an add-on download failure.
243- _addonDownloadFailureMessageTitle = pgettext ("addonStore" , "Add-on download failure" )
253+ def _handleCertVerificationError (
254+ self ,
255+ exception : requests .exceptions .SSLError ,
256+ listItem : "AddonListItemVM[_AddonStoreModel]" ,
257+ ) -> os .PathLike | None :
258+ import wx
259+ from gui .message import messageBox , DisplayableError
260+
261+ if _is_cert_verification_error (exception ):
262+ cert = _getCertificate (listItem .model .URL )
263+ certFingerprint = hashlib .sha256 (cert ).hexdigest ()
264+
265+ if (
266+ messageBox (
267+ message = pgettext (
268+ "addonStore" ,
269+ # Translators: A message to the user if an add-on download fails.
270+ # url is replaced with the base URL of the add-on download e.g. (github.com).
271+ # fingerprint is replaced with the SHA256 fingerprint of the certificate.
272+ "The website where you are downloading the add-on from has a certificate that is not trusted. "
273+ "Do you want to trust the root certificate for {url}? "
274+ "This will allow you to download add-ons from this website in the future. "
275+ "Only do this if you trust the website. "
276+ "The certificate's SHA256 fingerprint is: {fingerprint}. " ,
277+ ).format (url = urlparse (listItem .model .URL ).netloc , fingerprint = certFingerprint ),
278+ caption = self ._addonDownloadFailureMessageTitle ,
279+ style = wx .OK | wx .CANCEL | wx .CENTRE | wx .ICON_WARNING ,
280+ )
281+ == wx .OK
282+ ):
283+ _updateWindowsRootCertificates (cert )
284+ return self ._download (listItem )
285+ else :
286+ return None # The download was cancelled
287+ else :
288+ log .debugWarning (f"Unable to download addon file: { exception } " )
289+ raise DisplayableError (
290+ pgettext (
291+ "addonStore" ,
292+ # Translators: A message to the user if an add-on download fails
293+ "Unable to download add-on: {name}" ,
294+ ).format (name = listItem .model .displayName ),
295+ self ._addonDownloadFailureMessageTitle ,
296+ )
297+
298+ def _download (self , listItem : "AddonListItemVM[_AddonStoreModel]" ) -> os .PathLike | None :
299+ from gui .message import DisplayableError
244300
245301 addonData = listItem .model
246302 log .debug (f"starting download: { addonData .addonId } " )
@@ -257,6 +313,8 @@ def _download(self, listItem: "AddonListItemVM[_AddonStoreModel]") -> Optional[o
257313 try :
258314 if not self ._downloadAddonToPath (listItem , inProgressFilePath ):
259315 return None # The download was cancelled
316+ except requests .exceptions .SSLError as e :
317+ return self ._handleCertVerificationError (e , listItem )
260318 except requests .exceptions .RequestException as e :
261319 log .debugWarning (f"Unable to download addon file: { e } " )
262320 raise DisplayableError (
@@ -265,7 +323,7 @@ def _download(self, listItem: "AddonListItemVM[_AddonStoreModel]") -> Optional[o
265323 # Translators: A message to the user if an add-on download fails
266324 "Unable to download add-on: {name}" ,
267325 ).format (name = addonData .displayName ),
268- _addonDownloadFailureMessageTitle ,
326+ self . _addonDownloadFailureMessageTitle ,
269327 )
270328 except OSError as e :
271329 log .debugWarning (f"Unable to save addon file ({ inProgressFilePath } ): { e } " )
@@ -275,7 +333,7 @@ def _download(self, listItem: "AddonListItemVM[_AddonStoreModel]") -> Optional[o
275333 # Translators: A message to the user if an add-on download fails
276334 "Unable to save add-on as a file: {name}" ,
277335 ).format (name = addonData .displayName ),
278- _addonDownloadFailureMessageTitle ,
336+ self . _addonDownloadFailureMessageTitle ,
279337 )
280338 if not self ._checkChecksum (inProgressFilePath , addonData ):
281339 with self .DOWNLOAD_LOCK :
@@ -287,7 +345,7 @@ def _download(self, listItem: "AddonListItemVM[_AddonStoreModel]") -> Optional[o
287345 # Translators: A message to the user if an add-on download is not safe
288346 "Add-on download not safe: checksum failed for {name}" ,
289347 ).format (name = addonData .displayName ),
290- _addonDownloadFailureMessageTitle ,
348+ self . _addonDownloadFailureMessageTitle ,
291349 )
292350 log .debug (f"Download complete: { inProgressFilePath } " )
293351 with self .DOWNLOAD_LOCK :
0 commit comments