Skip to content

Commit bfdfe5b

Browse files
committed
feat(updates): implement production-ready update system with security and reliability features
Upgrade the software update subsystem from prototype to production-ready with comprehensive integrity verification, automatic retry, and resume capabilities. Security Enhancements: - SHA256 checksum verification for all downloaded files - File format validation (PE headers for Windows .exe) - Source URL validation (GitHub-only) - File permission hardening (chmod 0o600) - Document security trade-offs (DOS header vs full PE validation) Reliability Features: - Resume interrupted downloads using HTTP Range headers - Exponential backoff with jitter (3 retries, configurable) - Automatic retry on network/filesystem errors - Proper error classification and logging New Capabilities: - Direct wheel installation from GitHub release assets - Automatic checksum discovery from multiple asset formats (SHA256SUMS, *.sha256, checksums.txt) - Fallback to release notes for checksums - Corporate proxy support via environment variables Code Quality: - Extract magic values to named constants (DOWNLOAD_BLOCK_SIZE, PE_MAGIC_BYTES) - Replace generic exceptions with specific types - Use contextlib.suppress() for optional operations - Document non-cryptographic use of random.uniform() for timing jitter Testing: - Add checksum parsing tests for various release asset formats - Update download tests to use allow_resume parameter - All existing tests remain passing (70+ tests) Documentation: - Update ARCHITECTURE_1_software_update.md marking features as implemented - Change 14 items from ❌ TODO to ✅ IMPLEMENTED - Add Known Limitations section documenting security trade-offs Closes: Security requirements for production deployment Ref: ARCHITECTURE_1_software_update.md
1 parent 57684c1 commit bfdfe5b

11 files changed

+2152
-248
lines changed

ARCHITECTURE_1_software_update.md

Lines changed: 108 additions & 136 deletions
Large diffs are not rendered by default.

ardupilot_methodic_configurator/backend_internet.py

Lines changed: 344 additions & 92 deletions
Large diffs are not rendered by default.

ardupilot_methodic_configurator/data_model_software_updates.py

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import re
1515
from argparse import ArgumentParser
1616
from logging import basicConfig as logging_basicConfig
17-
from logging import debug as logging_error
17+
from logging import error as logging_error
1818
from logging import getLevelName as logging_getLevelName
1919
from logging import info as logging_info
2020
from logging import warning as logging_warning
@@ -29,6 +29,7 @@
2929
from ardupilot_methodic_configurator.backend_internet import (
3030
download_and_install_on_windows,
3131
download_and_install_pip_release,
32+
get_expected_sha256_from_release,
3233
get_release_info,
3334
webbrowser_open_url,
3435
)
@@ -62,40 +63,49 @@ def __init__(self) -> None:
6263
self.dialog: Optional[UpdateDialog] = None
6364

6465
def _perform_download(self, latest_release: dict[str, Any]) -> bool:
66+
result = False
6567
if platform.system() == "Windows":
6668
try:
6769
# Look for .exe files first
6870
exe_assets = [
6971
asset for asset in latest_release.get("assets", []) if asset.get("name", "").lower().endswith(".exe")
7072
]
7173

74+
asset = None
7275
if exe_assets:
7376
asset = exe_assets[0] # Use the first .exe file
7477
elif latest_release.get("assets"):
7578
asset = latest_release["assets"][0] # Fallback to first asset
79+
80+
if asset is not None:
81+
expected_sha256 = get_expected_sha256_from_release(latest_release, asset["name"])
82+
result = download_and_install_on_windows(
83+
download_url=asset["browser_download_url"],
84+
file_name=asset["name"],
85+
progress_callback=self.dialog.update_progress if self.dialog else None,
86+
expected_sha256=expected_sha256,
87+
)
7688
else:
7789
logging_error(_("No suitable assets found for Windows installation"))
78-
return False
79-
80-
return download_and_install_on_windows(
81-
download_url=asset["browser_download_url"],
82-
file_name=asset["name"],
83-
progress_callback=self.dialog.update_progress if self.dialog else None,
84-
)
90+
result = False
8591
except (KeyError, IndexError) as e:
8692
logging_error(_("Error accessing release assets: %s"), e)
87-
return False
93+
result = False
8894
except Exception as e: # pylint: disable=broad-exception-caught
8995
logging_error(_("Error during Windows download: %s"), e)
90-
return False
96+
result = False
97+
else:
98+
# For Linux/macOS, install from PyPI using pip
99+
try:
100+
result = (
101+
download_and_install_pip_release(progress_callback=self.dialog.update_progress if self.dialog else None)
102+
== 0
103+
)
104+
except Exception as e: # pylint: disable=broad-exception-caught
105+
logging_error(_("Error during pip installation: %s"), e)
106+
result = False
91107

92-
try:
93-
return (
94-
download_and_install_pip_release(progress_callback=self.dialog.update_progress if self.dialog else None) == 0
95-
)
96-
except Exception as e: # pylint: disable=broad-exception-caught
97-
logging_error(_("Error during pip installation: %s"), e)
98-
return False
108+
return result
99109

100110
def check_and_update(self, latest_release: dict[str, Any], current_version_str: str) -> bool:
101111
try:

0 commit comments

Comments
 (0)