Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ python3 -I tools/i18n.py check # language files: unused strings, los

pugwash flags: `--quiet --no-check --debug --no-colour --force-colour --no-log --offline --no-harbour`.

To test on a device: `./do_release.sh test`, copy `dist/PortMaster.zip` over (stream it over ssh: `scp` has truncated files on some devices), then on the device run `./PortMaster/harbourmaster --no-check install ./PortMaster.zip` (the `./` prefix makes it install a local file). `harbourmaster install` also takes a URL: `<url>/X.zip.md5` fetches the md5 file and then `X.zip`, and `<url>/X.zip?md5=<hex>` checks the download against that md5 (the `md5` parameter isn't sent to the server); both must agree when given together (`raw_download` in `source/__init__.py`).
To test on a device: `./do_release.sh test`, copy `dist/PortMaster.zip` over (stream it over ssh: `scp` has truncated files on some devices), then on the device run `./PortMaster/harbourmaster --no-check install ./PortMaster.zip` (the `./` prefix makes it install a local file). `harbourmaster install` also takes a URL: `<url>/X.zip.md5` fetches the md5 file and then `X.zip`, and `<url>/X.zip?md5=<hex>` checks the download against that md5 (the `md5` parameter isn't sent to the server). Given together, `<url>/X.zip.md5?md5=<hex>` downloads `X.zip` straight away and checks it against the link's md5, without fetching the md5 file; older PortMasters fetch the md5 file and then `X.zip`, so DeviceHub sends this form to every version (`raw_download` in `source/__init__.py`).

## Local-dev mode (HM_TESTING)

Expand Down
16 changes: 7 additions & 9 deletions PortMaster/pylibs/harbourmaster/source/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,13 @@ def raw_download(save_path, file_url, callback=None, file_name=None, md5_source=
if file_name is None:
file_name = url_info.path.rsplit('/', 1)[-1]

if file_name.endswith('.md5') or file_name.endswith('.md5sum'):
if (file_name.endswith('.md5') or file_name.endswith('.md5sum')) and md5_query is not None:
## "<url>/X.zip.md5?md5=<hex>": the md5 is right here, so the .md5 file isn't fetched; X.zip is.
## (Older PortMasters fetch the .md5 file and then X.zip, so the one link works for both.)
file_name = file_name.rsplit('.', 1)[0]
file_url = urlunparse(url_info._replace(path=url_info.path.rsplit('.', 1)[0]))

elif file_name.endswith('.md5') or file_name.endswith('.md5sum'):
## If it is an md5 file, we assume the actual zip is sans the md5/md5sum
md5_source = net.fetch_text(file_url)
if md5_source is None:
Expand All @@ -63,14 +69,6 @@ def raw_download(save_path, file_url, callback=None, file_name=None, md5_source=

md5_source = md5_source.strip().split(None, 1)[0].lower() if md5_source.strip() else ""

if md5_query is not None and md5_source != md5_query:
## Two answers to "what should it be": neither can be trusted.
if callback is not None:
callback.message_box(_("Download validation failed."))

logger.error(f"Unable to download file: {file_url!r} [md5 file {md5_source!r} != url md5 {md5_query!r}]")
return None

file_name = file_name.rsplit('.', 1)[0]
file_url = urlunparse(url_info._replace(path=url_info.path.rsplit('.', 1)[0]))

Expand Down
14 changes: 9 additions & 5 deletions tests/test_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,17 +235,21 @@ def test_raw_download_md5_file(tmp_path, callback, served):


def test_raw_download_md5_file_and_query(tmp_path, callback, served):
## The md5 is in the link, so the .md5 file isn't fetched: the zip is downloaded and checked.
## (DeviceHub sends these: older PortMasters fetch the .md5 file, then the zip, from the same link.)
url = f"https://example.com/port.zip.md5?md5={served.md5}"
assert hm_source.raw_download(tmp_path, url, callback=callback) is not None
assert served.got == ["https://example.com/port.zip"]
result = hm_source.raw_download(tmp_path, url, callback=callback)
assert result is not None and result['name'] == "port.zip"
assert served.fetched == [] and served.got == ["https://example.com/port.zip"]


def test_raw_download_md5_file_and_query_disagree(tmp_path, callback, served):
## Which one is right? Neither is trusted: nothing is downloaded.
def test_raw_download_md5_file_and_wrong_query(tmp_path, callback, served):
## The link's md5 is what the download is checked against.
result = hm_source.raw_download(tmp_path, f"https://example.com/port.zip.md5?md5={'0' * 32}", callback=callback)

assert result is None and served.got == []
assert result is None and served.fetched == []
assert callback.message_boxes == ["Download validation failed."]
assert not (tmp_path / "port.zip").exists()


def test_raw_download_no_md5(tmp_path, callback, served):
Expand Down
Loading