128 lines
3.5 KiB
GDScript
128 lines
3.5 KiB
GDScript
class_name ServerTrustStore
|
|
extends Node
|
|
|
|
const FORMAT_VERSION: int = 1
|
|
enum Verification {
|
|
FIRST_SEEN,
|
|
MATCH,
|
|
CHANGED,
|
|
}
|
|
|
|
var _records: Dictionary = {}
|
|
var _loaded: bool = false
|
|
var _write_blocked: bool = false
|
|
var _store_path := ""
|
|
var _expected_hash := ""
|
|
var _data_root: PlayerDataRoot
|
|
|
|
|
|
func configure_storage(path: String, data_root: PlayerDataRoot) -> void:
|
|
_store_path = path
|
|
_data_root = data_root
|
|
|
|
|
|
func verify(endpoint: ConnectionEndpoint, fingerprint: String) -> Verification:
|
|
_ensure_loaded()
|
|
if endpoint == null or not endpoint.is_valid():
|
|
return Verification.CHANGED
|
|
var record: Dictionary = _records.get(endpoint.normalized_display, {})
|
|
if record.is_empty():
|
|
return Verification.FIRST_SEEN
|
|
return (
|
|
Verification.MATCH
|
|
if record.get("fingerprint") == fingerprint
|
|
else Verification.CHANGED
|
|
)
|
|
|
|
|
|
func get_record(endpoint: ConnectionEndpoint) -> Dictionary:
|
|
_ensure_loaded()
|
|
if endpoint == null:
|
|
return {}
|
|
return Dictionary(_records.get(endpoint.normalized_display, {})).duplicate(true)
|
|
|
|
|
|
func trust(
|
|
endpoint: ConnectionEndpoint,
|
|
fingerprint: String,
|
|
server_name: String = "",
|
|
) -> bool:
|
|
_ensure_loaded()
|
|
if (
|
|
endpoint == null
|
|
or not endpoint.is_valid()
|
|
or not NetworkIdentityCrypto.valid_fingerprint(fingerprint)
|
|
):
|
|
return false
|
|
var now := int(Time.get_unix_time_from_system())
|
|
var previous: Dictionary = _records.get(endpoint.normalized_display, {})
|
|
_records[endpoint.normalized_display] = {
|
|
"route_kind": "direct",
|
|
"normalized_host": endpoint.host,
|
|
"port": endpoint.port,
|
|
"normalized_endpoint": endpoint.normalized_display,
|
|
"fingerprint": fingerprint,
|
|
"first_seen_unix": int(previous.get("first_seen_unix", now)),
|
|
"last_seen_unix": now,
|
|
"last_observed_server_name": server_name.left(80),
|
|
"trust_state": "pinned",
|
|
}
|
|
return _save()
|
|
|
|
|
|
func touch(endpoint: ConnectionEndpoint) -> void:
|
|
_ensure_loaded()
|
|
if endpoint == null:
|
|
return
|
|
var record: Dictionary = _records.get(endpoint.normalized_display, {})
|
|
if record.is_empty():
|
|
return
|
|
record["last_seen_unix"] = int(Time.get_unix_time_from_system())
|
|
_save()
|
|
|
|
|
|
func _ensure_loaded() -> void:
|
|
if _loaded:
|
|
return
|
|
_loaded = true
|
|
if _store_path.is_empty() or not FileAccess.file_exists(_store_path):
|
|
return
|
|
var file := FileAccess.open(_store_path, FileAccess.READ)
|
|
if file == null:
|
|
return
|
|
var json := JSON.new()
|
|
if json.parse(file.get_as_text()) != OK or typeof(json.data) != TYPE_DICTIONARY:
|
|
return
|
|
var data: Dictionary = json.data
|
|
if data.get("format_version") != FORMAT_VERSION:
|
|
_write_blocked = true
|
|
return
|
|
if typeof(data.get("records")) != TYPE_ARRAY:
|
|
return
|
|
for value: Variant in data["records"]:
|
|
if typeof(value) != TYPE_DICTIONARY:
|
|
continue
|
|
var record: Dictionary = value
|
|
var endpoint := str(record.get("normalized_endpoint", ""))
|
|
var fingerprint := str(record.get("fingerprint", ""))
|
|
if not endpoint.is_empty() and NetworkIdentityCrypto.valid_fingerprint(fingerprint):
|
|
_records[endpoint] = record.duplicate(true)
|
|
_expected_hash = PortableFileGuard.hash_file(_store_path)
|
|
|
|
|
|
func _save() -> bool:
|
|
if _write_blocked:
|
|
return false
|
|
var bytes := JSON.stringify({
|
|
"format_version": FORMAT_VERSION,
|
|
"records": _records.values(),
|
|
}, "\t").to_utf8_buffer()
|
|
var result := PortableFileGuard.write_guarded(
|
|
_store_path, bytes, _expected_hash, _data_root.conflict_directory(),
|
|
_data_root.device_id,
|
|
)
|
|
if bool(result.get("conflict", false)):
|
|
_data_root.report_conflict(str(result.get("message", "")), str(result.get("conflict_path", "")))
|
|
if bool(result.get("ok", false)):
|
|
_expected_hash = str(result["hash"])
|
|
return bool(result.get("ok", false))
|