Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement checking for the integrity of downloaded remote releases. #11

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
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
145 changes: 131 additions & 14 deletions src/components/editors/remote/remote_editor_download/remote_editor_download.gd
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,26 @@ extends PanelContainer
const uuid = preload("res://addons/uuid.gd")

signal downloaded(abs_zip_path: String)
signal integrity_check_completed(passed: bool)

const _CHECKSUM_FILENAME = "SHA512-SUMS.txt"

var _retry_callback
var _url: String
var _fallback_url: String = "":
set(new_fallback):
if "tuxfamily" in new_fallback:
_mirror_switch_button.text = "Switch to TuxFamily"
elif "github" in new_fallback:
_mirror_switch_button.text = "Switch to GitHub"
elif new_fallback == "":
pass
else:
assert(false, "unknown fallback")
_fallback_url = new_fallback
var _target_abs_dir: String
var _file_name: String
var _integrity_check_thread: Thread

@onready var _progress_bar: ProgressBar = get_node("%ProgressBar")
@onready var _status: Label = get_node("%Status")
Expand All @@ -13,6 +31,7 @@ var _retry_callback
@onready var _title_label: Label = %TitleLabel
@onready var _install_button: Button = %InstallButton
@onready var _retry_button: Button = %RetryButton
@onready var _mirror_switch_button: Button = %MirrorSwitchButton


func _ready() -> void:
Expand All @@ -24,30 +43,31 @@ func _ready() -> void:

_retry_button.pressed.connect(func():
_remove_downloaded_file()
_integrity_check_thread = null
if _retry_callback:
_retry_callback.call()
)

_install_button.pressed.connect(func():
downloaded.emit(_download.download_file)
)

_mirror_switch_button.pressed.connect(func():
assert(_fallback_url)
start(_fallback_url, _target_abs_dir, _file_name, _url)
)

integrity_check_completed.connect(_on_integrity_check_completed)


func start(url, target_abs_dir, file_name, tux_fallback = ""):
func start(url, target_abs_dir, file_name, fallback_url = ""):
var download_completed_callback = func(result: int, response_code: int,
headers, body, download_completed_callback: Callable):
headers, body):
# https://github.com/godotengine/godot/blob/a7583881af5477cd73110cc859fecf7ceaf39bd7/editor/plugins/asset_library_editor_plugin.cpp#L316
var host = url
var error_text = null
var status = ""

if ((result != HTTPRequest.RESULT_SUCCESS or response_code != 200)
and "github.com" in url and tux_fallback):
print("Failure! Falling back to TuxFamily.")
_download.request_completed.disconnect(download_completed_callback)
start(tux_fallback, target_abs_dir, file_name, "")
return

match result:
HTTPRequest.RESULT_CHUNKED_BODY_SIZE_MISMATCH, HTTPRequest.RESULT_CONNECTION_ERROR, HTTPRequest.RESULT_BODY_SIZE_LIMIT_EXCEEDED:
error_text = "Connection error, prease try again."
Expand Down Expand Up @@ -84,18 +104,44 @@ func start(url, target_abs_dir, file_name, tux_fallback = ""):
$AcceptErrorDialog.dialog_text = "Download error:" + "\n" + error_text
$AcceptErrorDialog.popup_centered()
_retry_button.show()
if _fallback_url:
_mirror_switch_button.show()
_status.text = status
else:
_install_button.disabled = false
_status.text = "Ready to install"
downloaded.emit(_download.download_file)
_status.text = "Checking file integrity..."
var checksum_url: String = _url.get_base_dir().path_join(_CHECKSUM_FILENAME)
var checksum_downloader = HTTPRequest.new()
checksum_downloader.timeout = 10
checksum_downloader.download_chunk_size = 2048
checksum_downloader.download_file = _target_abs_dir.get_base_dir() \
.path_join(_CHECKSUM_FILENAME)
add_child(checksum_downloader)
print("Downloading ", checksum_url)
checksum_downloader.request(checksum_url)
var sig_result = await checksum_downloader.request_completed
var request_result = sig_result[0]
var request_response_code = sig_result[1]
checksum_downloader.queue_free()
if request_result != HTTPRequest.RESULT_SUCCESS \
or request_response_code != 200:
integrity_check_completed.emit(true, false)
else:
_dismiss_button.disabled = true
_dismiss_button.hide()
_integrity_check_thread = Thread.new()
_integrity_check_thread.start(_check_file_integrity)

assert(target_abs_dir.ends_with("/"))
print("Downloading " + url)

_retry_callback = func(): start(url, target_abs_dir, file_name)
_url = url
_target_abs_dir = target_abs_dir
_file_name = file_name
_fallback_url = fallback_url
_retry_callback = func(): start(url, target_abs_dir, file_name, fallback_url)

_retry_button.hide()
_mirror_switch_button.hide()
_install_button.disabled = true
_progress_bar.modulate = Color(1, 1, 1, 1)
_title_label.text = file_name
Expand All @@ -114,7 +160,7 @@ func start(url, target_abs_dir, file_name, tux_fallback = ""):
_status.text = "Something went wrong."
return

_download.request_completed.connect(download_completed_callback.bind(download_completed_callback))
_download.request_completed.connect(download_completed_callback)

#TODO handle deadlock
while _download.get_http_client_status() != HTTPClient.STATUS_DISCONNECTED:
Expand Down Expand Up @@ -151,8 +197,79 @@ func _notification(what: int) -> void:
_remove_downloaded_file()


## Checks integrity of the downloaded file by veryfing that the SHA512
## checksum is correct. Downloads SHA512-SUMS.txt to do so.[br]
##
## The authenticity of that checksum cannot be verified, however.[br]
##
## Failing to download SHA512-SUMS.txt is treated as success, because, again,
## this method does not verify authenticity of the release.[br]
##
## This is supposed to be run by a separate thread to avoid freezing the main
## thread. Emits [signal integrity_check_completed] when done, with
## [code]passed[/code] set to [code]true[/code] if successful,
## [code]false[/code] otherwise.
func _check_file_integrity() -> void:
var globalized_directory_path = ProjectSettings.globalize_path(_target_abs_dir)
match OS.get_name():
"Linux", "OpenBSD", "FreeBSD", "NetBSD", "BSD":
var status = OS.execute("bash", ["-c", "cd " + globalized_directory_path
+ " && sha512sum -c --ignore-missing --status "
+ _CHECKSUM_FILENAME])
call_deferred("emit_signal", "integrity_check_completed",
status == 0 or status == 127)
"Windows":
var certutil_output = []
OS.execute("certutil", ["-hashfile",
globalized_directory_path.path_join(_file_name),
"SHA512"], certutil_output)
var output_lines = certutil_output[0].split("\n")
if len(output_lines) <= 2:
call_deferred("emit_signal", "integrity_check_completed", true)

var obtained_sum = output_lines[1].strip_edges()
if not obtained_sum.is_valid_hex_number():
call_deferred("emit_signal", "integrity_check_completed", true)

var checksum_file_contents = FileAccess.open(_target_abs_dir.path_join(
_CHECKSUM_FILENAME), FileAccess.READ).get_as_text()

call_deferred("emit_signal", "integrity_check_completed",
obtained_sum + " " + _file_name in checksum_file_contents)
"macOS":
call_deferred("emit_signal", "integrity_check_completed", true)
_:
call_deferred("emit_signal", "integrity_check_completed", true)


func _on_integrity_check_completed(passed: bool, from_thread: bool = true) -> void:
assert(_integrity_check_thread == null or not _integrity_check_thread.is_alive())
_dismiss_button.show()
_dismiss_button.disabled = false
if _integrity_check_thread == null:
return
if passed:
_install_button.disabled = false
_status.text = "Ready to install"
downloaded.emit(_download.download_file)
else:
$AcceptErrorDialog.dialog_text = "Integrity check failed!\n" + \
"Retry or use another mirror."
$AcceptErrorDialog.popup_centered()
_retry_button.show()
if _fallback_url:
_mirror_switch_button.show()
_integrity_check_thread.wait_to_finish()
_integrity_check_thread = null


func _remove_downloaded_file():
if _download.download_file:
DirAccess.remove_absolute(
ProjectSettings.globalize_path(_download.download_file)
)
var sum_file_path = _target_abs_dir.path_join(_CHECKSUM_FILENAME)
if FileAccess.file_exists(sum_file_path):
DirAccess.remove_absolute(
ProjectSettings.globalize_path(sum_file_path)
)
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ layout_mode = 2
layout_mode = 2
size_flags_horizontal = 3

[node name="MirrorSwitchButton" type="Button" parent="MarginContainer/HBoxContainer/VBoxContainer/HBoxContainer2"]
unique_name_in_owner = true
visible = false
layout_mode = 2

[node name="RetryButton" type="Button" parent="MarginContainer/HBoxContainer/VBoxContainer/HBoxContainer2"]
unique_name_in_owner = true
layout_mode = 2
Expand Down