Add password protection to server saves (SHA-256 client hash, lock icons)
This commit is contained in:
@@ -1056,6 +1056,10 @@
|
||||
!e.hasOwnProperty("primaryMapData") || !e.hasOwnProperty("subMapData")
|
||||
);
|
||||
}
|
||||
static async sha256_hex(str) {
|
||||
const buf = await window.crypto.subtle.digest("SHA-256", new TextEncoder().encode(str));
|
||||
return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
static compressJSObjectToString(e) {
|
||||
return btoa(
|
||||
n.arraybufferToString(
|
||||
@@ -5625,7 +5629,7 @@
|
||||
() => {
|
||||
E.exportSavegamePrivateBin();
|
||||
},
|
||||
)}"><i class="fas fa-file-export"></i> ${e.T("settings-share-save-button", "Export save without file")}</button>\n <div id="savegameQR"></div>\n --\x3e\n </div>\n <div class="form-group">\n <h5 class="py-2 text-warning">Server Saves</h5>\n <div class="input-group mb-2">\n <input type="text" class="form-control bg-dark text-white" id="named-save-input" placeholder="Save name...">\n <div class="input-group-append">\n <button class="btn btn-outline-success" onclick="${wi.register(() => { E.exportNamedSave(); })}"><i class="fas fa-cloud-upload-alt"></i> Save on server</button>\n </div>\n </div>\n <small class="form-text text-muted mb-3">Upload a complete export to the server under a custom name.</small>\n <div class="input-group mt-2">\n <select class="form-control bg-dark text-white" id="server-saves-select"><option value="">Loading saves...</option></select>\n <div class="input-group-append">\n <button class="btn btn-outline-warning" onclick="${wi.register(() => { E.importNamedSave(); })}"><i class="fas fa-cloud-download-alt"></i> Load from server</button>\n </div>\n </div>\n <small class="form-text text-muted">Load a previously saved game from the server. This will overwrite your current progress.</small>\n </div>\n </div>\n </div>`;
|
||||
)}"><i class="fas fa-file-export"></i> ${e.T("settings-share-save-button", "Export save without file")}</button>\n <div id="savegameQR"></div>\n --\x3e\n </div>\n <div class="form-group">\n <h5 class="py-2 text-warning">Server Saves</h5>\n <div class="input-group mb-1">\n <input type="text" class="form-control bg-dark text-white" id="named-save-input" placeholder="Save name...">\n <div class="input-group-append">\n <button class="btn btn-outline-success" onclick="${wi.register(() => { E.exportNamedSave(); })}"><i class="fas fa-cloud-upload-alt"></i> Save on server</button>\n </div>\n </div>\n <input type="password" class="form-control bg-dark text-white mb-2" id="named-save-password" placeholder="Password (optional)">\n <small class="form-text text-muted mb-3">Upload a complete export to the server under a custom name.</small>\n <div class="input-group mt-2 mb-1">\n <select class="form-control bg-dark text-white" id="server-saves-select"><option value="">Loading saves...</option></select>\n <div class="input-group-append">\n <button class="btn btn-outline-warning" onclick="${wi.register(() => { E.importNamedSave(); })}"><i class="fas fa-cloud-download-alt"></i> Load from server</button>\n </div>\n </div>\n <input type="password" class="form-control bg-dark text-white mb-1" id="named-load-password" placeholder="Password (if protected)">\n <small class="form-text text-muted">Load a previously saved game from the server. This will overwrite your current progress.</small>\n </div>\n </div>\n </div>`;
|
||||
}
|
||||
static mapManagementFormView() {
|
||||
const e = s.getInstance();
|
||||
@@ -5875,16 +5879,21 @@
|
||||
const orig_compress = n.getSetting("compressExports");
|
||||
n.setSetting("completeExports", true);
|
||||
n.setSetting("compressExports", true);
|
||||
const password = $("#named-save-password").val();
|
||||
try {
|
||||
const data = await _.getSavegameData(error_obj);
|
||||
const compressed = n.compressJSObjectToString(data);
|
||||
const blob = new Blob([compressed], { type: "application/octet-stream" });
|
||||
const headers = { "Content-Type": "application/octet-stream" };
|
||||
if (password) headers["X-Password-Hash"] = await n.sha256_hex(password);
|
||||
await fetch(`https://pu-saves.burrson.de/saves/${encodeURIComponent(save_name)}`, {
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
headers,
|
||||
body: blob,
|
||||
});
|
||||
n.showToast("Save stored", `"${save_name}" was saved to the server.`, "success");
|
||||
const label = password ? " (protected)" : "";
|
||||
n.showToast("Save stored", `"${save_name}"${label} was saved to the server.`, "success");
|
||||
} catch (e) {
|
||||
n.showToast("Save failed", "Could not store the save on the server.", "danger");
|
||||
}
|
||||
@@ -5894,14 +5903,17 @@
|
||||
static async populateServerSaves() {
|
||||
try {
|
||||
const res = await fetch("https://pu-saves.burrson.de/saves", { mode: "cors" });
|
||||
const saves = await res.json();
|
||||
const data = await res.json();
|
||||
const select = $("#server-saves-select");
|
||||
if (!select.length) return;
|
||||
select.empty();
|
||||
if (saves.length === 0) {
|
||||
if (data.saves.length === 0) {
|
||||
select.append('<option value="">No saves found</option>');
|
||||
} else {
|
||||
saves.forEach(name => select.append(`<option value="${name}">${name}</option>`));
|
||||
data.saves.forEach(name => {
|
||||
const is_protected = data.protected.includes(name);
|
||||
select.append(`<option value="${name}">${is_protected ? "🔒 " : ""}${name}</option>`);
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
$("#server-saves-select").empty().append('<option value="">Could not load saves</option>');
|
||||
@@ -5910,13 +5922,21 @@
|
||||
static async importNamedSave() {
|
||||
const save_name = $("#server-saves-select").val();
|
||||
if (!save_name) return;
|
||||
const password = $("#named-load-password").val();
|
||||
try {
|
||||
const res = await fetch(`https://pu-saves.burrson.de/saves/${encodeURIComponent(save_name)}`, { mode: "cors" });
|
||||
const headers = {};
|
||||
if (password) headers["X-Password-Hash"] = await n.sha256_hex(password);
|
||||
const res = await fetch(`https://pu-saves.burrson.de/saves/${encodeURIComponent(save_name)}`, { mode: "cors", headers });
|
||||
if (res.status === 401) {
|
||||
const msg = await res.text();
|
||||
n.showToast("Access denied", msg.includes("required") ? "This save is password protected." : "Wrong password.", "danger");
|
||||
return;
|
||||
}
|
||||
if (!res.ok) throw "Not found";
|
||||
const text = await res.text();
|
||||
if (n.hasScript(text)) throw "Script found!";
|
||||
const data = JSON.parse(n.decompressJsonFromString(text));
|
||||
E.importSaveGame(data);
|
||||
const parsed = JSON.parse(n.decompressJsonFromString(text));
|
||||
E.importSaveGame(parsed);
|
||||
} catch (e) {
|
||||
n.showToast("Load failed", "Could not load the save from the server.", "danger");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user