class_name ServerStore extends RefCounted ## Gespeicherte Admin-Server des Editors (Name, Adresse, Port, https, Token). ## ## Das Token liegt NICHT im Klartext auf der Platte: die Datei ist mit einem ## aus Rechner-ID + Nutzerordner abgeleiteten Schlüssel verschlüsselt. Das hält ## Zufallsleser, Cloud-Sync und Screenshots von Ordnern raus – gegen Schadsoftware ## mit Zugriff auf deinen Benutzer schützt es nicht (dafür gibt es keine ## plattformübergreifende Lösung ohne Passwort-Manager). Tokens werden nie geloggt. const DEFAULT_PATH := "user://admin_server.dat" const DEFAULT_PORT := 24580 const MAX_SERVERS := 50 var path: String = DEFAULT_PATH var servers: Array = [] # [{id, name, host, port, tls, token}] var last_error: String = "" func _init(p: String = DEFAULT_PATH) -> void: path = p func _key() -> String: return ("svr-editor-admin|%s|%s" % [OS.get_unique_id(), OS.get_user_data_dir()]).sha256_text() func load_store() -> void: servers.clear() last_error = "" if not FileAccess.file_exists(path): return var fa := FileAccess.open_encrypted_with_pass(path, FileAccess.READ, _key()) if fa == null: last_error = "Gespeicherte Server konnten nicht gelesen werden (andere Installation oder beschädigte Datei)." return var parsed: Variant = JSON.parse_string(fa.get_as_text()) if parsed is Array: for e in parsed: if e is Dictionary and str(e.get("host", "")) != "": servers.append({ "id": str(e.get("id", _new_id())), "name": str(e.get("name", e["host"])), "host": str(e["host"]), "port": int(e.get("port", DEFAULT_PORT)), "tls": bool(e.get("tls", false)), "token": str(e.get("token", ""))}) func save_store() -> bool: var fa := FileAccess.open_encrypted_with_pass(path, FileAccess.WRITE, _key()) if fa == null: last_error = "Server-Liste konnte nicht gespeichert werden (Fehler %d)." % FileAccess.get_open_error() return false fa.store_string(JSON.stringify(servers)) return true ## Neuer oder aktualisierter Eintrag. Rückgabe: id ("" = Eingabe ungültig → last_error). func upsert(entry: Dictionary) -> String: var err := AdminClient.check_address(str(entry.get("host", "")), int(entry.get("port", 0))) if err != "": last_error = err return "" var e := { "id": str(entry.get("id", "")), "name": str(entry.get("name", "")).strip_edges(), "host": str(entry["host"]).strip_edges(), "port": int(entry["port"]), "tls": bool(entry.get("tls", false)), "token": str(entry.get("token", "")).strip_edges()} if e["name"] == "": e["name"] = "%s:%d" % [e["host"], e["port"]] if e["id"] == "": if servers.size() >= MAX_SERVERS: last_error = "Maximal %d gespeicherte Server." % MAX_SERVERS return "" e["id"] = _new_id() servers.append(e) else: var i := index_of(e["id"]) if i < 0: servers.append(e) else: servers[i] = e save_store() return e["id"] func remove(id: String) -> bool: var i := index_of(id) if i < 0: return false servers.remove_at(i) save_store() return true func index_of(id: String) -> int: for i in servers.size(): if str(servers[i]["id"]) == id: return i return -1 func get_entry(id: String) -> Dictionary: var i := index_of(id) return servers[i].duplicate() if i >= 0 else {} ## Für Anzeige/Logs: nie das Token. func public_view(id: String) -> Dictionary: var e := get_entry(id) e.erase("token") e["has_token"] = str(get_entry(id).get("token", "")) != "" return e static func _new_id() -> String: return "s%d_%d" % [int(Time.get_unix_time_from_system()), randi() % 100000]