Refactor global styles and enhance MITM functionality
- Updated global CSS to implement a new brand color palette and improve light/dark theme consistency. - Enhanced the MitmServerCard component to provide clearer user feedback regarding admin privileges. - Filtered LLM combos in the CombosPage to ensure only relevant data is displayed. - Improved APIPageClient layout for better usability and visual consistency. - Added functionality to save and load DNS tool states in the MITM manager. - Updated OAuth configuration URLs for Qwen to reflect the new endpoint structure. - Refined tunnel management logic to improve reliability and user experience.
This commit is contained in:
@@ -65,6 +65,14 @@ function execWithPassword(command, password) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Trim trailing blank lines/whitespace, ensure file ends with exactly one newline.
|
||||
*/
|
||||
function normalizeHostsContent(content) {
|
||||
const eol = IS_WIN ? "\r\n" : "\n";
|
||||
return content.replace(/[\r\n\s]+$/g, "") + eol;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush DNS cache (macOS/Linux)
|
||||
*/
|
||||
@@ -120,19 +128,26 @@ async function addDNSEntry(tool, sudoPassword) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = entriesToAdd.map(h => `127.0.0.1 ${h}`).join("\n");
|
||||
|
||||
try {
|
||||
if (IS_WIN) {
|
||||
const toAppend = entriesToAdd.map(h => `127.0.0.1 ${h}`).join("`r`n");
|
||||
// Single elevated script: append to hosts + flush DNS (1 UAC popup, or zero if admin)
|
||||
// Read → trim → append → write (avoids stacked blank lines from Add-Content)
|
||||
const current = fs.readFileSync(HOSTS_FILE, "utf8");
|
||||
const trimmed = current.replace(/[\r\n\s]+$/g, "");
|
||||
const toAppend = entriesToAdd.map(h => `127.0.0.1 ${h}`).join("\r\n");
|
||||
const next = `${trimmed}\r\n${toAppend}\r\n`;
|
||||
const script = `
|
||||
Add-Content -LiteralPath ${quotePs(HOSTS_FILE)} -Value ${quotePs(toAppend)}
|
||||
Set-Content -LiteralPath ${quotePs(HOSTS_FILE)} -Value ${quotePs(next)} -NoNewline
|
||||
ipconfig /flushdns | Out-Null
|
||||
`;
|
||||
await runElevatedPowerShell(script);
|
||||
} else {
|
||||
await execWithPassword(`echo "${entries}" >> ${HOSTS_FILE}`, sudoPassword);
|
||||
const current = fs.readFileSync(HOSTS_FILE, "utf8");
|
||||
const trimmed = current.replace(/[\r\n\s]+$/g, "");
|
||||
const toAppend = entriesToAdd.map(h => `127.0.0.1 ${h}`).join("\n");
|
||||
const next = `${trimmed}\n${toAppend}\n`;
|
||||
// Use tee via sudo to overwrite atomically — escape single quotes in content
|
||||
const escaped = next.replace(/'/g, "'\\''");
|
||||
await execWithPassword(`printf '%s' '${escaped}' | tee ${HOSTS_FILE} > /dev/null`, sudoPassword);
|
||||
await flushDNS(sudoPassword);
|
||||
}
|
||||
log(`🌐 DNS ${tool}: ✅ added ${entriesToAdd.join(", ")}`);
|
||||
@@ -157,26 +172,20 @@ async function removeDNSEntry(tool, sudoPassword) {
|
||||
|
||||
try {
|
||||
if (IS_WIN) {
|
||||
// Build PowerShell list literal of hosts to strip
|
||||
const hostsList = entriesToRemove.map(quotePs).join(",");
|
||||
const current = fs.readFileSync(HOSTS_FILE, "utf8");
|
||||
const filtered = current.split(/\r?\n/).filter(l => !entriesToRemove.some(h => l.includes(h))).join("\r\n");
|
||||
const next = filtered.replace(/[\r\n\s]+$/g, "") + "\r\n";
|
||||
const script = `
|
||||
$hosts = @(${hostsList})
|
||||
$lines = Get-Content -LiteralPath ${quotePs(HOSTS_FILE)}
|
||||
$filtered = $lines | Where-Object {
|
||||
$line = $_
|
||||
-not ($hosts | Where-Object { $line -match [regex]::Escape($_) })
|
||||
}
|
||||
Set-Content -LiteralPath ${quotePs(HOSTS_FILE)} -Value $filtered
|
||||
Set-Content -LiteralPath ${quotePs(HOSTS_FILE)} -Value ${quotePs(next)} -NoNewline
|
||||
ipconfig /flushdns | Out-Null
|
||||
`;
|
||||
await runElevatedPowerShell(script);
|
||||
} 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);
|
||||
}
|
||||
const current = fs.readFileSync(HOSTS_FILE, "utf8");
|
||||
const filtered = current.split(/\r?\n/).filter(l => !entriesToRemove.some(h => l.includes(h))).join("\n");
|
||||
const next = filtered.replace(/[\r\n\s]+$/g, "") + "\n";
|
||||
const escaped = next.replace(/'/g, "'\\''");
|
||||
await execWithPassword(`printf '%s' '${escaped}' | tee ${HOSTS_FILE} > /dev/null`, sudoPassword);
|
||||
await flushDNS(sudoPassword);
|
||||
}
|
||||
log(`🌐 DNS ${tool}: ✅ removed ${entriesToRemove.join(", ")}`);
|
||||
@@ -210,8 +219,9 @@ function removeAllDNSEntriesSync() {
|
||||
const content = fs.readFileSync(HOSTS_FILE, "utf8");
|
||||
const eol = IS_WIN ? "\r\n" : "\n";
|
||||
const filtered = content.split(/\r?\n/).filter(l => !allHosts.some(h => l.includes(h))).join(eol);
|
||||
if (filtered === content) return;
|
||||
fs.writeFileSync(HOSTS_FILE, filtered, "utf8");
|
||||
const next = filtered.replace(/[\r\n\s]+$/g, "") + eol;
|
||||
if (next === content) return;
|
||||
fs.writeFileSync(HOSTS_FILE, next, "utf8");
|
||||
if (IS_WIN) {
|
||||
try { execSync("ipconfig /flushdns", { windowsHide: true, stdio: "ignore" }); } catch { /* ignore */ }
|
||||
} else if (IS_MAC) {
|
||||
|
||||
@@ -5,7 +5,8 @@ const os = require("os");
|
||||
const net = require("net");
|
||||
const https = require("https");
|
||||
const crypto = require("crypto");
|
||||
const { addDNSEntry, removeDNSEntry, removeAllDNSEntries, checkAllDNSStatus, TOOL_HOSTS, isSudoAvailable, isSudoPasswordRequired } = require("./dns/dnsConfig");
|
||||
const { addDNSEntry, removeDNSEntry, removeAllDNSEntries, removeAllDNSEntriesSync, checkAllDNSStatus, TOOL_HOSTS, isSudoAvailable, isSudoPasswordRequired } = require("./dns/dnsConfig");
|
||||
const { isAdmin } = require("./winElevated.js");
|
||||
|
||||
const IS_WIN = process.platform === "win32";
|
||||
const IS_MAC = process.platform === "darwin";
|
||||
@@ -219,6 +220,55 @@ async function loadEncryptedPassword() {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveDnsToolState(tool, enabled) {
|
||||
if (!_updateSettings || !_getSettings) return;
|
||||
try {
|
||||
const s = await _getSettings();
|
||||
const next = { ...(s.dnsToolEnabled || {}), [tool]: enabled };
|
||||
await _updateSettings({ dnsToolEnabled: next });
|
||||
} catch (e) {
|
||||
err(`Failed to save DNS state: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDnsToolState() {
|
||||
if (!_getSettings) return {};
|
||||
try {
|
||||
const s = await _getSettings();
|
||||
return s.dnsToolEnabled || {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-apply DNS for tools previously enabled — called on app startup after MITM running.
|
||||
*/
|
||||
async function restoreToolDNS(sudoPassword) {
|
||||
const state = await loadDnsToolState();
|
||||
const password = sudoPassword || getCachedPassword() || await loadEncryptedPassword();
|
||||
for (const [tool, enabled] of Object.entries(state)) {
|
||||
if (!enabled || !TOOL_HOSTS[tool]) continue;
|
||||
try {
|
||||
await addDNSEntry(tool, password);
|
||||
} catch (e) {
|
||||
err(`DNS ${tool}: restore failed — ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has privilege to mutate hosts file.
|
||||
* Win: needs admin. Mac/Linux: root OR cached/encrypted sudo password.
|
||||
*/
|
||||
async function hasDnsPrivilege() {
|
||||
if (IS_WIN) return isAdmin();
|
||||
if (isAdmin()) return true;
|
||||
if (!isSudoPasswordRequired()) return true;
|
||||
const pwd = getCachedPassword() || await loadEncryptedPassword();
|
||||
return !!pwd;
|
||||
}
|
||||
|
||||
function checkPort443Free() {
|
||||
return new Promise((resolve) => {
|
||||
const tester = net.createServer();
|
||||
@@ -634,7 +684,8 @@ async function stopServer(sudoPassword) {
|
||||
// Direct fs write — bypass PowerShell to avoid parser pitfalls
|
||||
const content = fs.readFileSync(hostsFile, "utf8");
|
||||
const filtered = content.split(/\r?\n/).filter(l => !allHosts.some(h => l.includes(h))).join("\r\n");
|
||||
if (filtered !== content) fs.writeFileSync(hostsFile, filtered, "utf8");
|
||||
const next = filtered.replace(/[\r\n\s]+$/g, "") + "\r\n";
|
||||
if (next !== content) fs.writeFileSync(hostsFile, next, "utf8");
|
||||
try { require("child_process").execSync("ipconfig /flushdns", { windowsHide: true, stdio: "ignore" }); } catch { /* ignore */ }
|
||||
log("🌐 DNS: ✅ all tool hosts removed");
|
||||
} else {
|
||||
@@ -669,10 +720,10 @@ async function stopServer(sudoPassword) {
|
||||
async function enableToolDNS(tool, sudoPassword) {
|
||||
const status = await getMitmStatus();
|
||||
if (!status.running) throw new Error("MITM server is not running. Start the server first.");
|
||||
|
||||
// Use cached password if not provided
|
||||
|
||||
const password = sudoPassword || getCachedPassword() || await loadEncryptedPassword();
|
||||
await addDNSEntry(tool, password);
|
||||
await saveDnsToolState(tool, true);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@@ -680,9 +731,9 @@ async function enableToolDNS(tool, sudoPassword) {
|
||||
* Disable DNS for a specific tool
|
||||
*/
|
||||
async function disableToolDNS(tool, sudoPassword) {
|
||||
// Use cached password if not provided
|
||||
const password = sudoPassword || getCachedPassword() || await loadEncryptedPassword();
|
||||
await removeDNSEntry(tool, password);
|
||||
await saveDnsToolState(tool, false);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@@ -723,4 +774,7 @@ module.exports = {
|
||||
clearEncryptedPassword,
|
||||
isSudoPasswordRequired,
|
||||
initDbHooks,
|
||||
restoreToolDNS,
|
||||
hasDnsPrivilege,
|
||||
removeAllDNSEntriesSync,
|
||||
};
|
||||
|
||||
@@ -7,13 +7,15 @@ const IS_WIN = process.platform === "win32";
|
||||
* Uses `net session` which only succeeds when elevated.
|
||||
*/
|
||||
function isAdmin() {
|
||||
if (!IS_WIN) return false;
|
||||
try {
|
||||
execSync("net session >nul 2>&1", { windowsHide: true, stdio: "ignore" });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
if (IS_WIN) {
|
||||
try {
|
||||
execSync("net session >nul 2>&1", { windowsHide: true, stdio: "ignore" });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return typeof process.getuid === "function" && process.getuid() === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user