84 lines
3.2 KiB
JavaScript
84 lines
3.2 KiB
JavaScript
|
|
// Sites whose sign-in survives "Clear cookies on quit".
|
||
|
|
//
|
||
|
|
// The vault knows which sites have a saved login, but it is locked by the
|
||
|
|
// time Theseus quits (and often for the whole session). So main keeps a copy
|
||
|
|
// of just the hostnames here, refreshed whenever the vault is open, sealed
|
||
|
|
// with Electron safeStorage (DPAPI / Keychain / libsecret) so it is not a
|
||
|
|
// plain-text list of the user's accounts on disk. Without a real OS keystore
|
||
|
|
// nothing is stored and every cookie is cleared as before.
|
||
|
|
//
|
||
|
|
// keepOrigins() turns the list into the origins Session.clearData() should
|
||
|
|
// leave alone. Chromium matches cookies at the registrable-domain level, so
|
||
|
|
// one origin per host covers the site's cookies; storage (localStorage,
|
||
|
|
// IndexedDB) is per origin, so the bare and www. forms are both listed.
|
||
|
|
//
|
||
|
|
// File: { v: 1, data: <b64 safeStorage blob of JSON string[]> }
|
||
|
|
|
||
|
|
"use strict";
|
||
|
|
|
||
|
|
const fs = require("node:fs");
|
||
|
|
|
||
|
|
const HOST_RE = /^(?=.{1,253}$)[a-z0-9-]+(\.[a-z0-9-]+)*$/;
|
||
|
|
|
||
|
|
function normHost(h) {
|
||
|
|
const s = String(h || "").trim().toLowerCase().replace(/\.$/, "");
|
||
|
|
return HOST_RE.test(s) ? s : "";
|
||
|
|
}
|
||
|
|
|
||
|
|
function keepOrigins(hosts) {
|
||
|
|
const out = new Set();
|
||
|
|
for (const raw of hosts || []) {
|
||
|
|
const h = normHost(raw);
|
||
|
|
if (!h) continue;
|
||
|
|
// No www. twin for an IP address or a one-label host: Chromium rejects
|
||
|
|
// "www.127.0.0.1" as an origin, and one bad origin fails the whole call.
|
||
|
|
const plain = /^\d+(\.\d+){3}$/.test(h) || !h.includes(".");
|
||
|
|
for (const host of plain ? [h] : h.startsWith("www.") ? [h, h.slice(4)] : [h, "www." + h]) {
|
||
|
|
out.add("https://" + host);
|
||
|
|
out.add("http://" + host);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return [...out];
|
||
|
|
}
|
||
|
|
|
||
|
|
function createSigninSites({ file, safeStorage, log = () => {} }) {
|
||
|
|
const sealOk = () => {
|
||
|
|
try {
|
||
|
|
if (!safeStorage || !safeStorage.isEncryptionAvailable()) return false;
|
||
|
|
if (process.platform === "linux") {
|
||
|
|
const b = typeof safeStorage.getSelectedStorageBackend === "function" ? safeStorage.getSelectedStorageBackend() : "unknown";
|
||
|
|
if (b === "basic_text" || b === "unknown") return false;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
} catch { return false; }
|
||
|
|
};
|
||
|
|
let cache = null;
|
||
|
|
|
||
|
|
function load() {
|
||
|
|
if (cache) return cache.slice();
|
||
|
|
let list = [];
|
||
|
|
try {
|
||
|
|
const rec = JSON.parse(fs.readFileSync(file, "utf8"));
|
||
|
|
if (rec && rec.v === 1 && rec.data && sealOk()) list = JSON.parse(safeStorage.decryptString(Buffer.from(rec.data, "base64")));
|
||
|
|
} catch { list = []; }
|
||
|
|
cache = Array.isArray(list) ? list.map(normHost).filter(Boolean) : [];
|
||
|
|
return cache.slice();
|
||
|
|
}
|
||
|
|
|
||
|
|
function save(hosts) {
|
||
|
|
const list = [...new Set((hosts || []).map(normHost).filter(Boolean))].sort();
|
||
|
|
cache = list;
|
||
|
|
if (!sealOk()) { try { fs.unlinkSync(file); } catch {} return false; }
|
||
|
|
try {
|
||
|
|
const tmp = file + ".tmp";
|
||
|
|
fs.writeFileSync(tmp, JSON.stringify({ v: 1, data: safeStorage.encryptString(JSON.stringify(list)).toString("base64") }), { mode: 0o600 });
|
||
|
|
fs.renameSync(tmp, file);
|
||
|
|
return true;
|
||
|
|
} catch (e) { log("signin-sites: save failed:", e?.message || e); return false; }
|
||
|
|
}
|
||
|
|
|
||
|
|
return { load, save, keepOrigins: () => keepOrigins(load()) };
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = { createSigninSites, keepOrigins, normHost };
|