This commit is contained in:
decolua 2026-02-25 10:40:15 +07:00
parent 091ad3fc5d
commit 147fc168f9
4 changed files with 90 additions and 35 deletions

View file

@ -1,6 +1,6 @@
{ {
"name": "9router-app", "name": "9router-app",
"version": "0.2.95", "version": "0.2.98",
"description": "9Router web dashboard", "description": "9Router web dashboard",
"private": true, "private": true,
"scripts": { "scripts": {

View file

@ -4,6 +4,8 @@ const { exec } = require("child_process");
const { execWithPassword } = require("../dns/dnsConfig.js"); const { execWithPassword } = require("../dns/dnsConfig.js");
const IS_WIN = process.platform === "win32"; const IS_WIN = process.platform === "win32";
const IS_MAC = process.platform === "darwin";
const LINUX_CERT_DIR = "/usr/local/share/ca-certificates";
// Get SHA1 fingerprint from cert file using Node.js crypto // Get SHA1 fingerprint from cert file using Node.js crypto
function getCertFingerprint(certPath) { function getCertFingerprint(certPath) {
@ -16,10 +18,9 @@ function getCertFingerprint(certPath) {
* Check if certificate is already installed in system store * Check if certificate is already installed in system store
*/ */
async function checkCertInstalled(certPath) { async function checkCertInstalled(certPath) {
if (IS_WIN) { if (IS_WIN) return checkCertInstalledWindows(certPath);
return checkCertInstalledWindows(certPath); if (IS_MAC) return checkCertInstalledMac(certPath);
} return checkCertInstalledLinux();
return checkCertInstalledMac(certPath);
} }
function checkCertInstalledMac(certPath) { function checkCertInstalledMac(certPath) {
@ -60,8 +61,10 @@ async function installCert(sudoPassword, certPath) {
if (IS_WIN) { if (IS_WIN) {
await installCertWindows(certPath); await installCertWindows(certPath);
} else { } else if (IS_MAC) {
await installCertMac(sudoPassword, certPath); await installCertMac(sudoPassword, certPath);
} else {
await installCertLinux(sudoPassword, certPath);
} }
} }
@ -103,8 +106,10 @@ async function uninstallCert(sudoPassword, certPath) {
if (IS_WIN) { if (IS_WIN) {
await uninstallCertWindows(); await uninstallCertWindows();
} else { } else if (IS_MAC) {
await uninstallCertMac(sudoPassword, certPath); await uninstallCertMac(sudoPassword, certPath);
} else {
await uninstallCertLinux(sudoPassword);
} }
} }
@ -133,4 +138,32 @@ async function uninstallCertWindows() {
}); });
} }
function checkCertInstalledLinux() {
const certFile = `${LINUX_CERT_DIR}/9router-mitm.crt`;
return Promise.resolve(fs.existsSync(certFile));
}
async function installCertLinux(sudoPassword, certPath) {
const destFile = `${LINUX_CERT_DIR}/9router-mitm.crt`;
// Try update-ca-certificates (Debian/Ubuntu), fallback to update-ca-trust (Fedora/RHEL)
const cmd = `cp "${certPath}" "${destFile}" && (update-ca-certificates 2>/dev/null || update-ca-trust 2>/dev/null || true)`;
try {
await execWithPassword(cmd, sudoPassword);
console.log("✅ Installed certificate to Linux trust store");
} catch (error) {
throw new Error("Certificate install failed");
}
}
async function uninstallCertLinux(sudoPassword) {
const destFile = `${LINUX_CERT_DIR}/9router-mitm.crt`;
const cmd = `rm -f "${destFile}" && (update-ca-certificates 2>/dev/null || update-ca-trust 2>/dev/null || true)`;
try {
await execWithPassword(cmd, sudoPassword);
console.log("✅ Uninstalled certificate from Linux trust store");
} catch (error) {
throw new Error("Failed to uninstall certificate");
}
}
module.exports = { installCert, uninstallCert, checkCertInstalled }; module.exports = { installCert, uninstallCert, checkCertInstalled };

View file

@ -1,9 +1,11 @@
const { exec, spawn } = require("child_process"); const { exec, spawn } = require("child_process");
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
const os = require("os");
const TARGET_HOST = "daily-cloudcode-pa.googleapis.com"; const TARGET_HOST = "daily-cloudcode-pa.googleapis.com";
const IS_WIN = process.platform === "win32"; const IS_WIN = process.platform === "win32";
const IS_MAC = process.platform === "darwin";
const HOSTS_FILE = IS_WIN const HOSTS_FILE = IS_WIN
? path.join(process.env.SystemRoot || "C:\\Windows", "System32", "drivers", "etc", "hosts") ? path.join(process.env.SystemRoot || "C:\\Windows", "System32", "drivers", "etc", "hosts")
: "/etc/hosts"; : "/etc/hosts";
@ -81,8 +83,11 @@ async function addDNSEntry(sudoPassword) {
// Flush DNS cache // Flush DNS cache
if (IS_WIN) { if (IS_WIN) {
await execElevatedWindows("ipconfig /flushdns"); await execElevatedWindows("ipconfig /flushdns");
} else { } else if (IS_MAC) {
await execWithPassword("dscacheutil -flushcache && killall -HUP mDNSResponder", sudoPassword); await execWithPassword("dscacheutil -flushcache && killall -HUP mDNSResponder", sudoPassword);
} else {
// Linux: try systemd-resolved, fall back silently
await execWithPassword("resolvectl flush-caches 2>/dev/null || true", sudoPassword);
} }
console.log(`✅ Added DNS entry: ${entry}`); console.log(`✅ Added DNS entry: ${entry}`);
} catch (error) { } catch (error) {
@ -102,23 +107,37 @@ async function removeDNSEntry(sudoPassword) {
try { try {
if (IS_WIN) { if (IS_WIN) {
// Windows: read, filter, write back via elevated PowerShell // Read in Node, filter, write to temp file, then elevated-copy over hosts
const psScript = `(Get-Content '${HOSTS_FILE}') | Where-Object { $_ -notmatch '${TARGET_HOST}' } | Set-Content '${HOSTS_FILE}'`; const content = fs.readFileSync(HOSTS_FILE, "utf8");
const psCommand = `Start-Process powershell -ArgumentList '-Command','${psScript.replace(/'/g, "''")}' -Verb RunAs -Wait`; const filtered = content.split(/\r?\n/).filter(l => !l.includes(TARGET_HOST)).join("\r\n");
if (!filtered.trim() && content.trim()) {
throw new Error("Filtered hosts content is empty, aborting to prevent data loss");
}
const tmpFile = path.join(os.tmpdir(), "hosts_filtered.tmp");
fs.writeFileSync(tmpFile, filtered, "utf8");
// Use elevated cmd to copy temp file over hosts (safe: original untouched until copy succeeds)
const psCommand = `Start-Process cmd -ArgumentList '/c','copy /Y "${tmpFile}" "${HOSTS_FILE}"' -Verb RunAs -Wait`;
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
exec(`powershell -Command "${psCommand}"`, (error) => { exec(`powershell -Command "${psCommand}"`, (error) => {
try { fs.unlinkSync(tmpFile); } catch { /* ignore */ }
if (error) reject(new Error(`Failed to remove DNS entry: ${error.message}`)); if (error) reject(new Error(`Failed to remove DNS entry: ${error.message}`));
else resolve(); else resolve();
}); });
}); });
} else { } else {
await execWithPassword(`sed -i '' '/${TARGET_HOST}/d' ${HOSTS_FILE}`, sudoPassword); // sed -i '' is macOS syntax; Linux uses sed -i without the empty string arg
const sedCmd = IS_MAC
? `sed -i '' '/${TARGET_HOST}/d' ${HOSTS_FILE}`
: `sed -i '/${TARGET_HOST}/d' ${HOSTS_FILE}`;
await execWithPassword(sedCmd, sudoPassword);
} }
// Flush DNS cache // Flush DNS cache
if (IS_WIN) { if (IS_WIN) {
await execElevatedWindows("ipconfig /flushdns"); await execElevatedWindows("ipconfig /flushdns");
} else { } else if (IS_MAC) {
await execWithPassword("dscacheutil -flushcache && killall -HUP mDNSResponder", sudoPassword); await execWithPassword("dscacheutil -flushcache && killall -HUP mDNSResponder", sudoPassword);
} else {
await execWithPassword("resolvectl flush-caches 2>/dev/null || true", sudoPassword);
} }
console.log(`✅ Removed DNS entry for ${TARGET_HOST}`); console.log(`✅ Removed DNS entry for ${TARGET_HOST}`);
} catch (error) { } catch (error) {

View file

@ -389,13 +389,14 @@ async function startMitm(apiKey, sudoPassword) {
console.log("Starting MITM server..."); console.log("Starting MITM server...");
if (IS_WIN) { if (IS_WIN) {
// Launch elevated node via PowerShell RunAs (triggers UAC prompt) // Use cmd /c to set env vars inline before launching node (env vars survive RunAs)
const nodePath = process.execPath.replace(/'/g, "''"); const nodePath = process.execPath.replace(/"/g, '\\"');
const serverPath = SERVER_PATH.replace(/'/g, "''"); const serverPath = SERVER_PATH.replace(/"/g, '\\"');
const cmdLine = `set ROUTER_API_KEY=${apiKey}&& set NODE_ENV=production&& "${nodePath}" "${serverPath}"`;
serverProcess = spawn("powershell", [ serverProcess = spawn("powershell", [
"-NoProfile", "-Command", "-NoProfile", "-Command",
`$env:ROUTER_API_KEY='${apiKey}'; $env:NODE_ENV='production'; Start-Process '${nodePath}' -ArgumentList '''${serverPath}''' -Verb RunAs -WindowStyle Hidden` `Start-Process cmd -ArgumentList '/c','${cmdLine.replace(/'/g, "''")}' -Verb RunAs -WindowStyle Hidden`
], { stdio: "ignore", env: process.env }); ], { stdio: "ignore" });
} else { } else {
// sudo -S: read password from stdin, -E: preserve env vars // sudo -S: read password from stdin, -E: preserve env vars
// Pass ROUTER_API_KEY inline via env=... wrapper to avoid sudo stripping env // Pass ROUTER_API_KEY inline via env=... wrapper to avoid sudo stripping env
@ -413,23 +414,25 @@ async function startMitm(apiKey, sudoPassword) {
fs.writeFileSync(PID_FILE, String(serverPid)); fs.writeFileSync(PID_FILE, String(serverPid));
let startError = null; let startError = null;
serverProcess.stdout.on("data", (data) => { if (!IS_WIN) {
console.log(`[MITM Server] ${data.toString().trim()}`); serverProcess.stdout.on("data", (data) => {
}); console.log(`[MITM Server] ${data.toString().trim()}`);
serverProcess.stderr.on("data", (data) => { });
const msg = data.toString().trim(); serverProcess.stderr.on("data", (data) => {
// Capture meaningful errors (ignore sudo password prompt noise) const msg = data.toString().trim();
if (msg && !msg.includes("Password:") && !msg.includes("password for")) { // Capture meaningful errors (ignore sudo password prompt noise)
console.error(`[MITM Server Error] ${msg}`); if (msg && !msg.includes("Password:") && !msg.includes("password for")) {
startError = msg; console.error(`[MITM Server Error] ${msg}`);
} startError = msg;
}); }
serverProcess.on("exit", (code) => { });
console.log(`MITM server exited with code ${code}`); serverProcess.on("exit", (code) => {
serverProcess = null; console.log(`MITM server exited with code ${code}`);
serverPid = null; serverProcess = null;
try { fs.unlinkSync(PID_FILE); } catch { /* ignore */ } serverPid = null;
}); try { fs.unlinkSync(PID_FILE); } catch { /* ignore */ }
});
}
// Wait for server to be ready by polling health endpoint // Wait for server to be ready by polling health endpoint
const health = await pollMitmHealth(IS_WIN ? 12000 : 8000); const health = await pollMitmHealth(IS_WIN ? 12000 : 8000);