Skip to content

Commit 8a7529d

Browse files
Merge 38a684f into e06f2b8
2 parents e06f2b8 + 38a684f commit 8a7529d

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
@@ -121,15 +121,64 @@ def getQualifiedDriverClassNameForStats(cls):
121121
return "%s (core)" % name
122122

123123

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

126174

127175
def checkForUpdate(auto: bool = False) -> Optional[Dict]:
128176
"""Check for an updated version of NVDA.
129177
This will block, so it generally shouldn't be called from the main thread.
130-
@param auto: Whether this is an automatic check for updates.
131-
@return: Information about the update or C{None} if there is no update.
132-
@raise RuntimeError: If there is an error checking for an update.
178+
179+
:param auto: Whether this is an automatic check for updates.
180+
:return: Information about the update or None if there is no update.
181+
:raise RuntimeError: If there is an error checking for an update.
133182
"""
134183
allowUsageStats = config.conf["update"]["allowUsageStats"]
135184
# #11837: build version string, service pack, and product type manually
@@ -141,6 +190,7 @@ def checkForUpdate(auto: bool = False) -> Optional[Dict]:
141190
if winVersion.service_pack_minor != 0:
142191
winVersionText += ".%d" % winVersion.service_pack_minor
143192
winVersionText += " %s" % ("workstation", "domain controller", "server")[winVersion.product_type - 1]
193+
144194
params = {
145195
"autoCheck": auto,
146196
"allowUsageStats": allowUsageStats,
@@ -153,6 +203,7 @@ def checkForUpdate(auto: bool = False) -> Optional[Dict]:
153203
"x64": os.environ.get("PROCESSOR_ARCHITEW6432") == "AMD64",
154204
"osArchitecture": os.environ.get("PROCESSOR_ARCHITEW6432"),
155205
}
206+
156207
if auto and allowUsageStats:
157208
synthDriverClass = synthDriverHandler.getSynth().__class__
158209
brailleDisplayClass = braille.handler.display.__class__ if braille.handler else None
@@ -171,6 +222,7 @@ def checkForUpdate(auto: bool = False) -> Optional[Dict]:
171222
"outputBrailleTable": config.conf["braille"]["translationTable"] if brailleDisplayClass else None,
172223
}
173224
params.update(extraParams)
225+
174226
url = f"{_getCheckURL()}?{urllib.parse.urlencode(params)}"
175227
try:
176228
log.debug(f"Fetching update data from {url}")
@@ -183,25 +235,22 @@ def checkForUpdate(auto: bool = False) -> Optional[Dict]:
183235
# #4803: Windows fetches trusted root certificates on demand.
184236
# Python doesn't trigger this fetch (PythonIssue:20916), so try it ourselves
185237
_updateWindowsRootCertificates()
186-
# and then retry the update check.
187-
log.debug(f"Fetching update data from {url}")
238+
# Retry the update check
239+
log.debug(f"Retrying update check from {url}")
188240
res = urllib.request.urlopen(url, timeout=UPDATE_FETCH_TIMEOUT_S)
189241
else:
190242
raise
243+
191244
if res.code != 200:
192-
raise RuntimeError("Checking for update failed with code %d" % res.code)
193-
info = {}
194-
for line in res:
195-
# #9819: update description resource returns bytes, so make it Unicode.
196-
line = line.decode("utf-8").rstrip()
197-
try:
198-
key, val = line.split(": ", 1)
199-
except ValueError:
200-
raise RuntimeError("Error in update check output")
201-
info[key] = val
202-
if not info:
203-
return None
204-
return info
245+
raise RuntimeError(f"Checking for update failed with HTTP status code {res.code}.")
246+
247+
data = res.read().decode("utf-8") # Ensure the response is decoded correctly
248+
if not isValidUpdateMirrorResponse(data):
249+
raise RuntimeError(
250+
"The response from the update server is invalid. Please ensure the URL is correct and points to a valid NVDA update mirror.",
251+
)
252+
253+
return parseUpdateCheckResponse(data)
205254

206255

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