mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 20:00:47 +00:00
270 lines
9.2 KiB
JavaScript
270 lines
9.2 KiB
JavaScript
const { exec, spawn } = require("child_process");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const os = require("os");
|
|
|
|
// Per-tool DNS hosts mapping
|
|
const TOOL_HOSTS = {
|
|
antigravity: ["daily-cloudcode-pa.googleapis.com", "cloudcode-pa.googleapis.com"],
|
|
copilot: ["api.individual.githubcopilot.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";
|
|
|
|
/**
|
|
* Execute elevated PowerShell script on Windows via Start-Process -Verb RunAs.
|
|
* Only UAC consent dialog appears, no CMD/PS window popup.
|
|
*/
|
|
function executeElevatedPowerShell(psScriptPath, timeoutMs = 30000) {
|
|
const flagFile = path.join(os.tmpdir(), `ps_done_${Date.now()}.flag`);
|
|
const psSQ = (s) => s.replace(/'/g, "''");
|
|
|
|
let psContent = fs.readFileSync(psScriptPath, "utf8");
|
|
psContent += `\nSet-Content -Path '${psSQ(flagFile)}' -Value 'done' -Encoding UTF8\n`;
|
|
fs.writeFileSync(psScriptPath, psContent, "utf8");
|
|
|
|
const outerCmd = `Start-Process powershell -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-WindowStyle','Hidden','-File','${psSQ(psScriptPath)}' -Verb RunAs -WindowStyle Hidden`;
|
|
|
|
return new Promise((resolve, reject) => {
|
|
let settled = false;
|
|
const settle = (fn, arg) => { if (!settled) { settled = true; fn(arg); } };
|
|
|
|
exec(
|
|
`powershell -NoProfile -NonInteractive -WindowStyle Hidden -Command "${outerCmd}"`,
|
|
{ windowsHide: true },
|
|
() => {}
|
|
);
|
|
|
|
const deadline = Date.now() + timeoutMs;
|
|
const poll = () => {
|
|
if (settled) return;
|
|
if (fs.existsSync(flagFile)) {
|
|
try { fs.unlinkSync(flagFile); fs.unlinkSync(psScriptPath); } catch { /* ignore */ }
|
|
return settle(resolve);
|
|
}
|
|
if (Date.now() > deadline) {
|
|
try { fs.unlinkSync(psScriptPath); } catch { /* ignore */ }
|
|
return settle(reject, new Error("Timed out waiting for UAC confirmation"));
|
|
}
|
|
setTimeout(poll, 500);
|
|
};
|
|
setTimeout(poll, 300);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Execute command with sudo password via stdin (macOS/Linux only)
|
|
*/
|
|
function execWithPassword(command, password) {
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn("sudo", ["-S", "sh", "-c", command], {
|
|
stdio: ["pipe", "pipe", "pipe"]
|
|
});
|
|
|
|
let stdout = "";
|
|
let stderr = "";
|
|
child.stdout.on("data", (d) => { stdout += d; });
|
|
child.stderr.on("data", (d) => { stderr += d; });
|
|
|
|
child.on("close", (code) => {
|
|
if (code === 0) resolve(stdout);
|
|
else reject(new Error(stderr || `Exit code ${code}`));
|
|
});
|
|
|
|
child.stdin.write(`${password}\n`);
|
|
child.stdin.end();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Flush DNS cache (macOS/Linux)
|
|
*/
|
|
async function flushDNS(sudoPassword) {
|
|
if (IS_WIN) return; // Windows flushes inline via ipconfig
|
|
if (IS_MAC) {
|
|
await execWithPassword("dscacheutil -flushcache && killall -HUP mDNSResponder", sudoPassword);
|
|
} else {
|
|
await execWithPassword("resolvectl flush-caches 2>/dev/null || true", sudoPassword);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if DNS entry exists for a specific host
|
|
*/
|
|
function checkDNSEntry(host = null) {
|
|
try {
|
|
const hostsContent = fs.readFileSync(HOSTS_FILE, "utf8");
|
|
if (host) return hostsContent.includes(host);
|
|
// Legacy: check all antigravity hosts (backward compat)
|
|
return TOOL_HOSTS.antigravity.every(h => hostsContent.includes(h));
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check DNS status per tool — returns { [tool]: boolean }
|
|
*/
|
|
function checkAllDNSStatus() {
|
|
try {
|
|
const hostsContent = fs.readFileSync(HOSTS_FILE, "utf8");
|
|
const result = {};
|
|
for (const [tool, hosts] of Object.entries(TOOL_HOSTS)) {
|
|
result[tool] = hosts.every(h => hostsContent.includes(h));
|
|
}
|
|
return result;
|
|
} catch {
|
|
return Object.fromEntries(Object.keys(TOOL_HOSTS).map(t => [t, false]));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add DNS entries for a specific tool
|
|
*/
|
|
async function addDNSEntry(tool, sudoPassword) {
|
|
const hosts = TOOL_HOSTS[tool];
|
|
if (!hosts) throw new Error(`Unknown tool: ${tool}`);
|
|
|
|
const entriesToAdd = hosts.filter(h => !checkDNSEntry(h));
|
|
if (entriesToAdd.length === 0) {
|
|
console.log(`DNS entries for ${tool} already exist`);
|
|
return;
|
|
}
|
|
|
|
const entries = entriesToAdd.map(h => `127.0.0.1 ${h}`).join("\n");
|
|
|
|
try {
|
|
if (IS_WIN) {
|
|
const hostsPath = HOSTS_FILE.replace(/'/g, "''");
|
|
|
|
// Build PowerShell script with proper error handling
|
|
const scriptLines = [];
|
|
scriptLines.push(`$ErrorActionPreference = 'Stop'`);
|
|
scriptLines.push(`$hostsPath = '${hostsPath}'`);
|
|
scriptLines.push(`try {`);
|
|
scriptLines.push(` $hostsContent = Get-Content -Path $hostsPath -Raw -ErrorAction SilentlyContinue`);
|
|
scriptLines.push(` if (-not $hostsContent) { $hostsContent = '' }`);
|
|
|
|
for (const host of entriesToAdd) {
|
|
// Escape special regex chars in hostname
|
|
const escapedHost = host.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
scriptLines.push(` if ($hostsContent -notmatch '${escapedHost}') {`);
|
|
scriptLines.push(` Add-Content -Path $hostsPath -Value '127.0.0.1 ${host}' -Encoding UTF8 -ErrorAction Stop`);
|
|
scriptLines.push(` Write-Host "Added DNS entry: ${host}"`);
|
|
scriptLines.push(` } else {`);
|
|
scriptLines.push(` Write-Host "DNS entry already exists: ${host}"`);
|
|
scriptLines.push(` }`);
|
|
}
|
|
|
|
scriptLines.push(` ipconfig /flushdns | Out-Null`);
|
|
scriptLines.push(`} catch {`);
|
|
scriptLines.push(` Write-Error "Failed to add DNS: $_"`);
|
|
scriptLines.push(` exit 1`);
|
|
scriptLines.push(`}`);
|
|
|
|
const psScript = scriptLines.join("\n");
|
|
const tmpPs1 = path.join(os.tmpdir(), `mitm_dns_add_${Date.now()}.ps1`);
|
|
fs.writeFileSync(tmpPs1, psScript, "utf8");
|
|
|
|
await executeElevatedPowerShell(tmpPs1, 30000);
|
|
} else {
|
|
await execWithPassword(`echo "${entries}" >> ${HOSTS_FILE}`, sudoPassword);
|
|
await flushDNS(sudoPassword);
|
|
}
|
|
console.log(`✅ Added DNS entries for ${tool}: ${entriesToAdd.join(", ")}`);
|
|
} catch (error) {
|
|
const msg = error.message?.includes("incorrect password") ? "Wrong sudo password" : "Failed to add DNS entry";
|
|
throw new Error(msg);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove DNS entries for a specific tool
|
|
*/
|
|
async function removeDNSEntry(tool, sudoPassword) {
|
|
const hosts = TOOL_HOSTS[tool];
|
|
if (!hosts) throw new Error(`Unknown tool: ${tool}`);
|
|
|
|
const entriesToRemove = hosts.filter(h => checkDNSEntry(h));
|
|
if (entriesToRemove.length === 0) {
|
|
console.log(`DNS entries for ${tool} do not exist`);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (IS_WIN) {
|
|
const content = fs.readFileSync(HOSTS_FILE, "utf8");
|
|
const filtered = content.split(/\r?\n/).filter(l => !entriesToRemove.some(h => l.includes(h))).join("\r\n");
|
|
const tmpFile = path.join(os.tmpdir(), `hosts_filtered_${Date.now()}.tmp`);
|
|
fs.writeFileSync(tmpFile, filtered, "utf8");
|
|
|
|
const tmpEsc = tmpFile.replace(/'/g, "''");
|
|
const hostsEsc = HOSTS_FILE.replace(/'/g, "''");
|
|
|
|
// Build PowerShell script with proper error handling
|
|
const scriptLines = [];
|
|
scriptLines.push(`$ErrorActionPreference = 'Stop'`);
|
|
scriptLines.push(`try {`);
|
|
scriptLines.push(` Copy-Item -Path '${tmpEsc}' -Destination '${hostsEsc}' -Force -ErrorAction Stop`);
|
|
scriptLines.push(` Write-Host "Hosts file updated successfully"`);
|
|
scriptLines.push(` ipconfig /flushdns | Out-Null`);
|
|
scriptLines.push(` Write-Host "DNS cache flushed"`);
|
|
scriptLines.push(` Remove-Item '${tmpEsc}' -ErrorAction SilentlyContinue`);
|
|
scriptLines.push(`} catch {`);
|
|
scriptLines.push(` Write-Error "Failed to remove DNS: $_"`);
|
|
scriptLines.push(` Remove-Item '${tmpEsc}' -ErrorAction SilentlyContinue`);
|
|
scriptLines.push(` exit 1`);
|
|
scriptLines.push(`}`);
|
|
|
|
const psScript = scriptLines.join("\n");
|
|
const tmpPs1 = path.join(os.tmpdir(), `mitm_dns_remove_${Date.now()}.ps1`);
|
|
fs.writeFileSync(tmpPs1, psScript, "utf8");
|
|
|
|
await executeElevatedPowerShell(tmpPs1, 30000);
|
|
|
|
// Cleanup temp file if still exists
|
|
try { fs.unlinkSync(tmpFile); } catch { /* ignore */ }
|
|
} else {
|
|
for (const host of entriesToRemove) {
|
|
const sedCmd = IS_MAC
|
|
? `sed -i '' '/${host}/d' ${HOSTS_FILE}`
|
|
: `sed -i '/${host}/d' ${HOSTS_FILE}`;
|
|
await execWithPassword(sedCmd, sudoPassword);
|
|
}
|
|
await flushDNS(sudoPassword);
|
|
}
|
|
console.log(`✅ Removed DNS entries for ${tool}: ${entriesToRemove.join(", ")}`);
|
|
} catch (error) {
|
|
const msg = error.message?.includes("incorrect password") ? "Wrong sudo password" : "Failed to remove DNS entry";
|
|
throw new Error(msg);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove ALL tool DNS entries (used when stopping server)
|
|
*/
|
|
async function removeAllDNSEntries(sudoPassword) {
|
|
for (const tool of Object.keys(TOOL_HOSTS)) {
|
|
try {
|
|
await removeDNSEntry(tool, sudoPassword);
|
|
} catch (e) {
|
|
console.log(`[MITM] Warning: failed to remove DNS for ${tool}: ${e.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
TOOL_HOSTS,
|
|
addDNSEntry,
|
|
removeDNSEntry,
|
|
removeAllDNSEntries,
|
|
execWithPassword,
|
|
executeElevatedPowerShell,
|
|
checkDNSEntry,
|
|
checkAllDNSStatus,
|
|
};
|