Skip to content

Commit b58d91a

Browse files
Merge ed8efbe into 8c771f0
2 parents 8c771f0 + ed8efbe commit b58d91a

3 files changed

Lines changed: 76 additions & 19 deletions

File tree

source/gui/settingsDialogs.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -992,6 +992,7 @@ def onChangeMirrorURL(self, evt: wx.CommandEvent | wx.KeyEvent):
992992
configPath=("update", "serverURL"),
993993
helpId="SetURLDialog",
994994
urlTransformer=lambda url: f"{url}?versionType=stable",
995+
responseValidator=_isResponseUpdateMirrorValid,
995996
)
996997
ret = changeMirror.ShowModal()
997998
if ret == wx.ID_OK:
@@ -5589,3 +5590,10 @@ def _isResponseAddonStoreCacheHash(response: requests.Response) -> bool:
55895590
# While the NV Access Add-on Store cache hash is a git commit hash as a string, other implementations may use a different format.
55905591
# Therefore, we only check if the data is a non-empty string.
55915592
return isinstance(data, str) and bool(data)
5593+
5594+
5595+
def _isResponseUpdateMirrorValid(response: requests.Response) -> bool:
5596+
if not response.ok:
5597+
return False
5598+
5599+
return updateCheck .isValidUpdateMirrorResponse(response.text)

source/updateCheck.py

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -119,15 +119,64 @@ def getQualifiedDriverClassNameForStats(cls):
119119
return "%s (core)" % name
120120

121121

122+
def parseUpdateCheckResponse(data: str) -> dict[str, str] | None:
123+
"""
124+
Parses the update response and returns a dictionary with metadata.
125+
126+
:param data: The raw server response as a UTF-8 decoded string.
127+
:return: A dictionary containing the update metadata, or None if the format is invalid.
128+
"""
129+
if not data.strip():
130+
return None
131+
132+
metadata = {}
133+
for line in data.splitlines():
134+
try:
135+
key, val = line.split(": ", 1)
136+
metadata[key] = val
137+
except ValueError:
138+
return None # Invalid format
139+
140+
return metadata
141+
142+
143+
def isValidUpdateMirrorResponse(responseData: str) -> bool:
144+
"""
145+
Validates the response from an update mirror by ensuring it contains the required keys.
146+
147+
:param responseData: The raw server response as a UTF-8 decoded string.
148+
:return: True if the response is valid, False otherwise.
149+
"""
150+
required_keys = {"version", "launcherUrl", "apiVersion"}
151+
152+
parsedResponse = parseUpdateCheckResponse(responseData)
153+
if not parsedResponse:
154+
log.warning(
155+
"The response data could not be parsed. Ensure the update mirror returns data in the expected format.",
156+
)
157+
return False
158+
159+
missing_keys = required_keys - parsedResponse.keys()
160+
if missing_keys:
161+
log.warning(
162+
f"The response from the update server is missing the following required keys: {', '.join(missing_keys)}. "
163+
"Ensure the update mirror provides these keys.",
164+
)
165+
return False
166+
167+
return True
168+
169+
122170
UPDATE_FETCH_TIMEOUT_S = 30 # seconds
123171

124172

125173
def checkForUpdate(auto: bool = False) -> Optional[Dict]:
126174
"""Check for an updated version of NVDA.
127175
This will block, so it generally shouldn't be called from the main thread.
128-
@param auto: Whether this is an automatic check for updates.
129-
@return: Information about the update or C{None} if there is no update.
130-
@raise RuntimeError: If there is an error checking for an update.
176+
177+
:param auto: Whether this is an automatic check for updates.
178+
:return: Information about the update or None if there is no update.
179+
:raise RuntimeError: If there is an error checking for an update.
131180
"""
132181
allowUsageStats = config.conf["update"]["allowUsageStats"]
133182
# #11837: build version string, service pack, and product type manually
@@ -139,6 +188,7 @@ def checkForUpdate(auto: bool = False) -> Optional[Dict]:
139188
if winVersion.service_pack_minor != 0:
140189
winVersionText += ".%d" % winVersion.service_pack_minor
141190
winVersionText += " %s" % ("workstation", "domain controller", "server")[winVersion.product_type - 1]
191+
142192
params = {
143193
"autoCheck": auto,
144194
"allowUsageStats": allowUsageStats,
@@ -151,6 +201,7 @@ def checkForUpdate(auto: bool = False) -> Optional[Dict]:
151201
"x64": os.environ.get("PROCESSOR_ARCHITEW6432") == "AMD64",
152202
"osArchitecture": os.environ.get("PROCESSOR_ARCHITEW6432"),
153203
}
204+
154205
if auto and allowUsageStats:
155206
synthDriverClass = synthDriverHandler.getSynth().__class__
156207
brailleDisplayClass = braille.handler.display.__class__ if braille.handler else None
@@ -169,6 +220,7 @@ def checkForUpdate(auto: bool = False) -> Optional[Dict]:
169220
"outputBrailleTable": config.conf["braille"]["translationTable"] if brailleDisplayClass else None,
170221
}
171222
params.update(extraParams)
223+
172224
url = f"{_getCheckURL()}?{urllib.parse.urlencode(params)}"
173225
try:
174226
log.debug(f"Fetching update data from {url}")
@@ -181,25 +233,22 @@ def checkForUpdate(auto: bool = False) -> Optional[Dict]:
181233
# #4803: Windows fetches trusted root certificates on demand.
182234
# Python doesn't trigger this fetch (PythonIssue:20916), so try it ourselves
183235
_updateWindowsRootCertificates()
184-
# and then retry the update check.
185-
log.debug(f"Fetching update data from {url}")
236+
# Retry the update check
237+
log.debug(f"Retrying update data fetch from {url}")
186238
res = urllib.request.urlopen(url, timeout=UPDATE_FETCH_TIMEOUT_S)
187239
else:
188240
raise
241+
189242
if res.code != 200:
190-
raise RuntimeError("Checking for update failed with code %d" % res.code)
191-
info = {}
192-
for line in res:
193-
# #9819: update description resource returns bytes, so make it Unicode.
194-
line = line.decode("utf-8").rstrip()
195-
try:
196-
key, val = line.split(": ", 1)
197-
except ValueError:
198-
raise RuntimeError("Error in update check output")
199-
info[key] = val
200-
if not info:
201-
return None
202-
return info
243+
raise RuntimeError(f"Checking for update failed with HTTP status code {res.code}.")
244+
245+
data = res.read().decode("utf-8") # Ensure the response is decoded correctly
246+
if not isValidUpdateMirrorResponse(data):
247+
raise RuntimeError(
248+
"The response from the update server is invalid. Please ensure the URL is correct and points to a valid NVDA update mirror.",
249+
)
250+
251+
return parseUpdateCheckResponse(data)
203252

204253

205254
def _setStateToNone(_state):

user_docs/en/changes.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ To use this feature, "allow NVDA to control the volume of other applications" mu
1919
* NVDA can now report when a link destination points to the current page. (#141, @LeonarddeR, @nvdaes)
2020
* Added an action in the Add-on Store to cancel the install of add-ons. (#15578, @hwf1324)
2121
* Added an action in the Add-on Store to retry the installation if the download/installation of an add-on fails. (#17090, @hwf1324)
22-
* It is now possible to specify mirror URLs to use for NVDA updates and the Add-on Store. (#14974, #17151)
22+
* It is now possible to specify mirror URLs to use for NVDA updates and the Add-on Store. (#14974, #17151, #17310, @christopherpross)
2323
* The add-ons lists in the Add-on Store can be sorted by columns, including publication date, in ascending and descending order. (#15277, #16681, @nvdaes)
2424
* When decreasing or increasing the font size in LibreOffice Writer using the corresponding keyboard shortcuts, NVDA announces the new font size. (#6915, @michaelweghorn)
2525
* When applying the "Body Text" or a heading paragraph style using the corresponding keyboard shortcut in LibreOffice Writer 25.2 or newer, NVDA announces the new paragraph style. (#6915, @michaelweghorn)

0 commit comments

Comments
 (0)