diff --git a/main.js b/main.js index 7a96745..505c4e8 100644 --- a/main.js +++ b/main.js @@ -2983,6 +2983,11 @@ ipcMain.handle("collision-reset", () => { collisions = { byName: {}, byTld: {} } // this machine; Theseus itself uses its own in-process resolver so it's // unaffected. Toggling requires admin (tasks run as SYSTEM) — start/stop go // through an elevated powershell that UAC-prompts once per action. +// +// As of 0.3.23 Ariadne is NOT bundled inside Theseus. Install / Update stream +// AriadneResolver-Setup-.exe directly from silentmode.st and verify its +// SHA-256 against the on-site releases manifest before spawning it, so +// Ariadne's release cadence is decoupled from ours. const ARIADNE_TASKS = ["BNS Resolver Daemon", "BNS Sia Bridge"]; // Inno Setup's AppId + "_is1" is the uninstall registry key. Check both // native and WOW6432 in case Inno installed either way. @@ -2990,15 +2995,83 @@ const ARIADNE_UNINSTALL_KEYS = [ 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{7E7A5F1C-3B4E-4C8A-9E1D-ARIADNERSLVR}_is1', 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{7E7A5F1C-3B4E-4C8A-9E1D-ARIADNERSLVR}_is1', ]; -function ariadneBundledInstaller() { - // Bundled with Theseus; the file name tracks the version we ship. - const dir = app.isPackaged ? process.resourcesPath : path.join(__dirname, "build"); - try { - const hit = fs.readdirSync(dir).find((f) => /^AriadneResolver-Setup-.*\.exe$/i.test(f)); - if (hit) return { path: path.join(dir, hit), version: (hit.match(/-Setup-(.+)\.exe$/i) || [])[1] || null }; - } catch {} - return { path: null, version: null }; +const ARIADNE_MANIFEST_URL = "https://silentmode.st/releases-manifest.json"; +const ARIADNE_DL_ORIGIN = "https://dl.silentmode.st"; +const ARIADNE_MANIFEST_TTL_MS = 30 * 60 * 1000; +let ariadneManifestCache = null; // { at:number, entry:{version,filename,sha256,url}|null } + +// GET the releases manifest, find the win-x64 ariadne-resolver entry, return +// {version, filename, sha256, url}. Cached 30 min in-process; opening the +// Settings panel every few seconds does not spam silentmode.st. On any +// failure (offline, 5xx, malformed JSON) returns null and the caller shows +// bundledVersion:null / canUpdate:false gracefully. +function ariadneManifestFetch() { + const now = Date.now(); + if (ariadneManifestCache && now - ariadneManifestCache.at < ARIADNE_MANIFEST_TTL_MS) { + return Promise.resolve(ariadneManifestCache.entry); + } + return new Promise((resolve) => { + const https = require("node:https"); + const req = https.request(ARIADNE_MANIFEST_URL, { + method: "GET", timeout: 8000, + headers: { "user-agent": "TheseusNavigator/ariadne-updater" }, + }, (r) => { + if (r.statusCode !== 200) { r.resume(); ariadneManifestCache = { at: now, entry: null }; return resolve(null); } + const chunks = []; + r.on("data", (c) => chunks.push(c)); + r.on("end", () => { + try { + const j = JSON.parse(Buffer.concat(chunks).toString("utf8")); + const rel = (j.releases || []).find((x) => x.id === "ariadne-resolver" && x.platform === "win-x64"); + if (!rel) { ariadneManifestCache = { at: now, entry: null }; return resolve(null); } + const filename = Object.keys(rel.files || {}).find((f) => /^AriadneResolver-Setup-.*\.exe$/i.test(f)); + if (!filename) { ariadneManifestCache = { at: now, entry: null }; return resolve(null); } + const entry = { version: rel.version, filename, sha256: String(rel.files[filename] || "").toLowerCase(), url: ARIADNE_DL_ORIGIN + "/" + filename }; + ariadneManifestCache = { at: now, entry }; + resolve(entry); + } catch { ariadneManifestCache = { at: now, entry: null }; resolve(null); } + }); + }); + req.on("timeout", () => req.destroy(new Error("manifest timeout"))); + req.on("error", () => { ariadneManifestCache = { at: now, entry: null }; resolve(null); }); + req.end(); + }); } + +// Stream the .exe to a per-session temp file, hashing as we go. Reject on +// hash mismatch (and delete the file) so a wrong-hash binary is never spawned. +// The SHA-256 is authoritative because the manifest itself is served over +// HTTPS -- silentmode.st TLS -> manifest.json -> hash -> verified .exe. +function ariadneDownloadInstaller(entry) { + return new Promise((resolve, reject) => { + const https = require("node:https"); + const crypto = require("node:crypto"); + const dst = path.join(app.getPath("temp"), `ariadne-${entry.version}-${Date.now()}.exe`); + const req = https.request(entry.url, { + method: "GET", timeout: 60000, + headers: { "user-agent": "TheseusNavigator/ariadne-updater" }, + }, (r) => { + if (r.statusCode !== 200) { r.resume(); return reject(new Error(`download ${entry.url} -> HTTP ${r.statusCode}`)); } + const hash = crypto.createHash("sha256"); + const out = fs.createWriteStream(dst); + r.on("data", (c) => hash.update(c)); + r.pipe(out); + out.on("finish", () => { + const got = hash.digest("hex").toLowerCase(); + if (got !== entry.sha256) { + try { fs.unlinkSync(dst); } catch {} + return reject(new Error(`SHA-256 mismatch: got ${got}, want ${entry.sha256}`)); + } + resolve(dst); + }); + out.on("error", (e) => { try { fs.unlinkSync(dst); } catch {}; reject(e); }); + }); + req.on("timeout", () => req.destroy(new Error("download timeout"))); + req.on("error", reject); + req.end(); + }); +} + function ariadneQueryState() { return new Promise((resolve) => { const { spawn } = require("child_process"); @@ -3014,19 +3087,23 @@ function ariadneQueryState() { ], { windowsHide: true }); let out = ""; ps.stdout.on("data", (d) => { out += d; }); - ps.on("close", () => { + ps.on("close", async () => { const lines = out.trim().split(/\r?\n/).filter(Boolean); const map = Object.fromEntries(lines.map((l) => { const i = l.lastIndexOf("="); return [l.slice(0, i), l.slice(i + 1)]; })); const primary = map["BNS Resolver Daemon"]; const installedVersion = map.__VER__ || null; const quietUninstall = map.__UNINSTALL__ || null; - const bundled = ariadneBundledInstaller(); - const canUpdate = installedVersion && bundled.version && cmpVersions(bundled.version, installedVersion) > 0; + const latest = await ariadneManifestFetch(); + const latestVersion = latest ? latest.version : null; + const canUpdate = !!(installedVersion && latestVersion && cmpVersions(latestVersion, installedVersion) > 0); let state; if (!primary || primary === "MISSING") state = installedVersion ? "stopped" : "not-installed"; else if (primary === "Running") state = "running"; else state = "stopped"; - resolve({ state, installedVersion, bundledVersion: bundled.version, canUpdate, hasUninstaller: !!quietUninstall }); + // The "bundledVersion" field name is kept for renderer compatibility -- + // it now carries the latest version advertised by silentmode.st's + // releases manifest, not a version physically bundled with Theseus. + resolve({ state, installedVersion, bundledVersion: latestVersion, canUpdate, hasUninstaller: !!quietUninstall }); }); ps.on("error", () => resolve({ state: "not-installed", installedVersion: null, bundledVersion: null, canUpdate: false, hasUninstaller: false })); }); @@ -3049,19 +3126,22 @@ function ariadneSetState(on) { ps.on("error", (e) => reject(e)); }); } -// Run the bundled Ariadne installer silently, elevated. Inno Setup with -// /VERYSILENT /SUPPRESSMSGBOXES /NORESTART finishes without user interaction -// after the initial UAC prompt. -function ariadneInstall() { - const bundled = ariadneBundledInstaller(); - if (!bundled.path) return Promise.reject(new Error("bundled installer not found")); +// Fetch manifest -> download+verify AriadneResolver-Setup-.exe -> spawn +// Inno silently+elevated (/VERYSILENT /SUPPRESSMSGBOXES /NORESTART). The +// downloaded .exe is deleted whether the install succeeds or fails, so a +// wrong-hash abort never leaves a suspect binary behind. +async function ariadneInstall() { + const entry = await ariadneManifestFetch(); + if (!entry) throw new Error("could not reach silentmode.st releases manifest"); + const exePath = await ariadneDownloadInstaller(entry); const { spawn } = require("child_process"); return new Promise((resolve, reject) => { const ps = spawn("powershell.exe", ["-NoProfile", "-Command", - `Start-Process -FilePath '${bundled.path.replace(/'/g, "''")}' -ArgumentList '/VERYSILENT','/SUPPRESSMSGBOXES','/NORESTART' -Verb RunAs -Wait` + `Start-Process -FilePath '${exePath.replace(/'/g, "''")}' -ArgumentList '/VERYSILENT','/SUPPRESSMSGBOXES','/NORESTART' -Verb RunAs -Wait` ], { windowsHide: true }); - ps.on("close", (code) => code === 0 ? resolve(true) : reject(new Error("installer exited " + code))); - ps.on("error", reject); + const cleanup = () => { try { fs.unlinkSync(exePath); } catch {} }; + ps.on("close", (code) => { cleanup(); code === 0 ? resolve(true) : reject(new Error("installer exited " + code)); }); + ps.on("error", (e) => { cleanup(); reject(e); }); }); } // Run Inno's own quiet uninstaller. Reads the QuietUninstallString from the diff --git a/nsis/installer.nsh b/nsis/installer.nsh deleted file mode 100644 index fb9d2e2..0000000 --- a/nsis/installer.nsh +++ /dev/null @@ -1,100 +0,0 @@ -; Theseus Navigator NSIS include — bundles Ariadne's Thread as an opt-out. -; -; UI: a dedicated wizard page with a real checkbox (pre-checked) instead of a -; MessageBox popup. Shown FIRST in the install flow because electron-builder's -; template places our `!include` before its own MUI_PAGE_* inserts and NSIS -; processes page directives in file order — so this is the earliest hook -; we have without editing the template. Framed as "Options" so it reads -; naturally as a preamble before the standard Welcome/License/Directory pages. -; Skipped entirely (`Abort`) if Ariadne's Thread is already installed, so -; upgrades of Theseus don't nag. -; -; The uninstall side stays a symmetric MessageBox (a full custom uninstaller -; page is overkill for one yes/no during a rare event). - -!include "nsDialogs.nsh" -!include "LogicLib.nsh" -!include "MUI2.nsh" - -Var AriadneCheckbox -Var InstallAriadneFlag - -!define ARIADNE_INSTALLER "AriadneResolver-Setup-0.1.0.exe" -!define ARIADNE_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\{7E7A5F1C-3B4E-4C8A-9E1D-ARIADNERSLVR}_is1" - -Page custom AriadnePageCreate AriadnePageLeave - -Function AriadnePageCreate - ; Silent install (/S) — no user, no page. Default to "don't touch Ariadne"; - ; the whole point of /S is unattended, and existing Ariadne stays as-is. - ; nsDialogs::Create in silent mode returns a dialog handle we can't display, - ; so we short-circuit BEFORE calling any nsDialogs API. - IfSilent ariadne_page_silent ariadne_page_check - ariadne_page_silent: - StrCpy $InstallAriadneFlag "0" - Return - - ariadne_page_check: - ; Skip the page entirely if Ariadne's Thread is already on this machine — - ; nothing to offer. Registry lookup uses HKLM 64-bit view because Ariadne's - ; Inno script sets ArchitecturesInstallIn64BitMode=x64compatible. - SetRegView 64 - ReadRegStr $R0 HKLM "${ARIADNE_UNINST_KEY}" "UninstallString" - SetRegView lastused - StrCmp $R0 "" ariadne_page_show 0 - StrCpy $InstallAriadneFlag "0" - Abort - - ariadne_page_show: - !insertmacro MUI_HEADER_TEXT "Optional add-ons" "Choose whether to also install Ariadne's Thread alongside Theseus Navigator." - - nsDialogs::Create 1018 - Pop $0 - StrCmp $0 "error" ariadne_page_bail 0 - - ${NSD_CreateLabel} 0 0 100% 44u "Ariadne's Thread is a small system-wide resolver that lets BCDN — Bitcoin Cash Domain Names — work in Chrome, Edge, Firefox, and every other browser on this machine.$\r$\n$\r$\nWithout it, only Theseus resolves these names. You can install or remove it later." - Pop $0 - - ${NSD_CreateCheckbox} 0 60u 100% 12u "Install Ariadne's Thread (recommended)" - Pop $AriadneCheckbox - ${NSD_Check} $AriadneCheckbox - - ${NSD_CreateLabel} 0 80u 100% 20u "It runs a UAC (admin) prompt of its own during install. Theseus Navigator's own setup continues after this page." - Pop $0 - - nsDialogs::Show - Return - - ariadne_page_bail: - StrCpy $InstallAriadneFlag "0" - Abort -FunctionEnd - -Function AriadnePageLeave - ${NSD_GetState} $AriadneCheckbox $InstallAriadneFlag -FunctionEnd - -!macro customInstall - ; Read the state saved from AriadnePageLeave. BST_CHECKED == 1. - ${If} $InstallAriadneFlag == ${BST_CHECKED} - ; Ariadne's Inno installer runs its own UAC prompt (PrivilegesRequired=admin). - ; ExecWait blocks until Ariadne finishes or the user cancels UAC; either - ; way, Theseus install continues normally afterwards. - ExecWait '"$INSTDIR\resources\${ARIADNE_INSTALLER}" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART' - ${EndIf} -!macroend - -!macro customUnInstall - ; Only offer to remove Ariadne if it's actually installed. - SetRegView 64 - ReadRegStr $R0 HKLM "${ARIADNE_UNINST_KEY}" "UninstallString" - SetRegView lastused - StrCmp $R0 "" ariadne_skip_uninstall - - MessageBox MB_YESNO|MB_ICONQUESTION|MB_DEFBUTTON1 \ - "Also uninstall Ariadne's Thread?$\r$\n$\r$\nRemoving it makes Bitcoin Cash names stop resolving in Chrome, Edge, Firefox, and every other browser on this machine. (Theseus's own uninstall does not depend on this — say No to keep the system-wide resolver.)" \ - IDNO ariadne_skip_uninstall - ExecWait '$R0 /VERYSILENT /SUPPRESSMSGBOXES /NORESTART' - - ariadne_skip_uninstall: -!macroend