feat(update): support verified public HTTP downloads

This commit is contained in:
chengma
2026-07-13 16:19:03 +08:00
parent 9a458ee195
commit 74b98cd123
8 changed files with 234 additions and 37 deletions
+31
View File
@@ -1739,6 +1739,37 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertEqual("重试", dialog.action_button.text())
self.assertIsNone(dialog.thread)
def test_forced_update_dialog_only_warns_for_plain_http_download(self):
common = {
"current_version": "1.0.0",
"checked": True,
"forced": True,
"latest_version": "1.2.0",
"sha256": "a" * 64,
"size_bytes": 100,
"package_format": "cmshopee-portable-v1",
"updater_protocol": 1,
"min_updater_protocol": 1,
}
http_dialog = ForcedUpdateDialog(
update_check.UpdateCheckResult(
download_url="http://external.example.test/cmshopee.zip",
**common,
)
)
https_dialog = ForcedUpdateDialog(
update_check.UpdateCheckResult(
download_url="https://external.example.test/cmshopee.zip",
**common,
)
)
self.addCleanup(http_dialog.close)
self.addCleanup(https_dialog.close)
self.assertFalse(http_dialog.http_warning_label.isHidden())
self.assertIn("临时 HTTP 通道", http_dialog.http_warning_label.text())
self.assertTrue(https_dialog.http_warning_label.isHidden())
def test_startup_update_gate_fuses_a_previously_failed_release(self):
captured = []
+118 -8
View File
@@ -3,6 +3,7 @@ import io
import json
import tempfile
import unittest
import urllib.request
import zipfile
from pathlib import Path
@@ -37,7 +38,9 @@ class FakeOpener:
class UpdateInstallerTests(unittest.TestCase):
trusted_hosts = {"updates.example.test"}
@staticmethod
def public_resolver(_host, _port):
return ["93.184.216.34"]
def make_zip(self, root, version="1.2.3"):
package = Path(root) / "package"
@@ -80,13 +83,13 @@ class UpdateInstallerTests(unittest.TestCase):
staged = update_installer.prepare_update(
self.metadata(payload),
install_root,
trusted_hosts=self.trusted_hosts,
resolver=self.public_resolver,
opener=opener,
)
reused = update_installer.prepare_update(
self.metadata(payload),
install_root,
trusted_hosts=self.trusted_hosts,
resolver=self.public_resolver,
opener=opener,
)
@@ -97,15 +100,41 @@ class UpdateInstallerTests(unittest.TestCase):
pending = json.loads(staged.pending_path.read_text(encoding="utf-8"))
self.assertNotIn("download_url", pending)
def test_download_rejects_hash_size_http_and_untrusted_host(self):
def test_prepare_update_accepts_public_http_and_writes_verified_pending(self):
with tempfile.TemporaryDirectory() as temp_dir:
payload = self.make_zip(temp_dir)
install_root = Path(temp_dir) / "installed"
install_root.mkdir()
metadata = self.metadata(
payload,
download_url="http://downloads.external.test/releases/app.zip",
)
staged = update_installer.prepare_update(
metadata,
install_root,
resolver=self.public_resolver,
opener=FakeOpener(payload, url=metadata.download_url),
)
pending = json.loads(staged.pending_path.read_text(encoding="utf-8"))
self.assertEqual("verified", pending["stage"])
self.assertEqual(metadata.sha256, pending["sha256"])
def test_download_rejects_invalid_metadata_and_unsafe_urls(self):
with tempfile.TemporaryDirectory() as temp_dir:
payload = b"zip"
root = Path(temp_dir)
cases = (
self.metadata(payload, sha256=""),
self.metadata(payload, size_bytes=0),
self.metadata(payload, download_url="http://updates.example.test/app.zip"),
self.metadata(payload, download_url="https://other.example.test/app.zip"),
self.metadata(payload, download_url="file:///tmp/app.zip"),
self.metadata(payload, download_url="http://user:secret@example.test/app.zip"),
self.metadata(payload, download_url="http://127.0.0.1/app.zip"),
self.metadata(payload, download_url="http://10.0.0.8/app.zip"),
self.metadata(payload, download_url="http://[::1]/app.zip"),
self.metadata(payload, download_url="http://localhost/app.zip"),
self.metadata(payload, download_url="http://[invalid/app.zip"),
)
for metadata in cases:
with self.subTest(metadata=metadata):
@@ -113,10 +142,91 @@ class UpdateInstallerTests(unittest.TestCase):
update_installer.download_package(
metadata,
root,
trusted_hosts=self.trusted_hosts,
resolver=self.public_resolver,
opener=FakeOpener(payload),
)
def test_download_allows_public_http_https_domains_and_public_ip(self):
payload = b"zip"
urls = (
"http://cdn-one.example.test:8080/releases/app.zip",
"https://storage.other.test/app.zip?token=hidden",
"http://8.8.8.8/app.zip",
)
with tempfile.TemporaryDirectory() as temp_dir:
for index, url in enumerate(urls):
with self.subTest(url=url):
root = Path(temp_dir) / str(index)
downloaded = update_installer.download_package(
self.metadata(payload, download_url=url),
root,
resolver=self.public_resolver,
opener=FakeOpener(payload, url=url),
)
self.assertEqual(payload, downloaded.read_bytes())
def test_download_rejects_domain_resolving_to_non_public_address(self):
payload = b"zip"
with tempfile.TemporaryDirectory() as temp_dir:
with self.assertRaisesRegex(update_installer.UpdateInstallError, "本机或内网"):
update_installer.download_package(
self.metadata(payload, download_url="http://files.example.test/app.zip"),
temp_dir,
resolver=lambda _host, _port: ["192.168.1.20"],
opener=FakeOpener(payload, url="http://files.example.test/app.zip"),
)
def test_redirect_handler_allows_public_cross_domain_and_protocol_redirects(self):
handler = update_installer._SafeRedirectHandler(self.public_resolver)
request = urllib.request.Request("https://origin.example.test/app.zip")
for target in (
"http://cdn.example.test:8080/releases/app.zip",
"https://objects.other.test/app.zip",
):
with self.subTest(target=target):
redirected = handler.redirect_request(request, None, 302, "Found", {}, target)
self.assertEqual(target, redirected.full_url)
with self.assertRaisesRegex(update_installer.UpdateInstallError, "本机或内网"):
handler.redirect_request(
request,
None,
302,
"Found",
{},
"http://169.254.169.254/latest/meta-data",
)
def test_download_revalidates_final_response_url(self):
payload = b"zip"
with tempfile.TemporaryDirectory() as temp_dir:
with self.assertRaisesRegex(update_installer.UpdateInstallError, "本机或内网"):
update_installer.download_package(
self.metadata(payload, download_url="http://cdn.example.test/app.zip"),
temp_dir,
resolver=self.public_resolver,
opener=FakeOpener(payload, url="http://127.0.0.1/app.zip"),
)
def test_http_download_still_requires_matching_hash(self):
payload = b"zip"
metadata = self.metadata(
payload,
download_url="http://cdn.example.test/app.zip",
sha256="0" * 64,
)
with tempfile.TemporaryDirectory() as temp_dir:
with self.assertRaisesRegex(update_installer.UpdateInstallError, "完整性校验失败"):
update_installer.download_package(
metadata,
temp_dir,
resolver=self.public_resolver,
opener=FakeOpener(payload, url=metadata.download_url),
)
self.assertFalse(
(Path(temp_dir) / ".cmshopee-update/downloads/1.2.3.zip").exists()
)
def test_cancel_removes_partial_download(self):
with tempfile.TemporaryDirectory() as temp_dir:
payload = b"zip-content"
@@ -124,7 +234,7 @@ class UpdateInstallerTests(unittest.TestCase):
update_installer.download_package(
self.metadata(payload),
temp_dir,
trusted_hosts=self.trusted_hosts,
resolver=self.public_resolver,
opener=FakeOpener(payload),
cancelled=lambda: True,
)