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",
"version": "0.2.95",
"version": "0.2.98",
"description": "9Router web dashboard",
"private": true,
"scripts": {

View File

@@ -4,6 +4,8 @@ const { exec } = require("child_process");
const { execWithPassword } = require("../dns/dnsConfig.js");
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
function getCertFingerprint(certPath) {
@@ -16,10 +18,9 @@ function getCertFingerprint(certPath) {
* Check if certificate is already installed in system store
*/
async function checkCertInstalled(certPath) {
if (IS_WIN) {
return checkCertInstalledWindows(certPath);
}
return checkCertInstalledMac(certPath);
if (IS_WIN) return checkCertInstalledWindows(certPath);
if (IS_MAC) return checkCertInstalledMac(certPath);
return checkCertInstalledLinux();
}
function checkCertInstalledMac(certPath) {
@@ -60,8 +61,10 @@ async function installCert(sudoPassword, certPath) {
if (IS_WIN) {
await installCertWindows(certPath);
} else {
} else if (IS_MAC) {
await installCertMac(sudoPassword, certPath);
} else {
await installCertLinux(sudoPassword, certPath);
}
}
@@ -103,8 +106,10 @@ async function uninstallCert(sudoPassword, certPath) {
if (IS_WIN) {
await uninstallCertWindows();
} else {
} else if (IS_MAC) {
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 };

View File

@@ -1,9 +1,11 @@
const { exec, spawn } = require("child_process");
const fs = require("fs");
const path = require("path");
const os = require("os");
const TARGET_HOST = "daily-cloudcode-pa.googleapis.com";
const IS_WIN = process.platform === "win32";
const IS_MAC = process.platform === "darwin";
const HOSTS_FILE = IS_WIN
? path.join(process.env.SystemRoot || "C:\\Windows", "System32", "drivers", "etc", "hosts")
: "/etc/hosts";
@@ -81,8 +83,11 @@ async function addDNSEntry(sudoPassword) {
// Flush DNS cache
if (IS_WIN) {
await execElevatedWindows("ipconfig /flushdns");
} else {
} else if (IS_MAC) {
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}`);
} catch (error) {
@@ -102,23 +107,37 @@ async function removeDNSEntry(sudoPassword) {
try {
if (IS_WIN) {
// Windows: read, filter, write back via elevated PowerShell
const psScript = `(Get-Content '${HOSTS_FILE}') | Where-Object { $_ -notmatch '${TARGET_HOST}' } | Set-Content '${HOSTS_FILE}'`;
const psCommand = `Start-Process powershell -ArgumentList '-Command','${psScript.replace(/'/g, "''")}' -Verb RunAs -Wait`;
// Read in Node, filter, write to temp file, then elevated-copy over hosts
const content = fs.readFileSync(HOSTS_FILE, "utf8");
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) => {
exec(`powershell -Command "${psCommand}"`, (error) => {
try { fs.unlinkSync(tmpFile); } catch { /* ignore */ }
if (error) reject(new Error(`Failed to remove DNS entry: ${error.message}`));
else resolve();
});
});
} 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
if (IS_WIN) {
await execElevatedWindows("ipconfig /flushdns");
} else {
} else if (IS_MAC) {
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}`);
} catch (error) {

View File

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