From 1fa05eb2ab2e09ef552200c4b33c5a5ea09408e0 Mon Sep 17 00:00:00 2001 From: decolua Date: Tue, 14 Apr 2026 11:48:59 +0700 Subject: [PATCH] Refactor execSync and spawn calls to include windowsHide option for better compatibility on Windows environments. --- src/app/api/tunnel/tailscale-check/route.js | 5 ++- src/app/api/tunnel/tailscale-install/route.js | 2 +- src/lib/tunnel/cloudflared.js | 4 +- src/lib/tunnel/tailscale.js | 43 +++++++++++-------- src/mitm/cert/install.js | 4 +- src/mitm/dns/dnsConfig.js | 6 +-- src/mitm/manager.js | 13 +++--- src/mitm/server.js | 2 +- 8 files changed, 45 insertions(+), 34 deletions(-) diff --git a/src/app/api/tunnel/tailscale-check/route.js b/src/app/api/tunnel/tailscale-check/route.js index 3e5aee0..5443b22 100644 --- a/src/app/api/tunnel/tailscale-check/route.js +++ b/src/app/api/tunnel/tailscale-check/route.js @@ -6,7 +6,7 @@ import { isTailscaleInstalled, isTailscaleLoggedIn, TAILSCALE_SOCKET } from "@/l const EXTENDED_PATH = `/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:${process.env.PATH || ""}`; function hasBrew() { - try { execSync("which brew", { stdio: "ignore", env: { ...process.env, PATH: EXTENDED_PATH } }); return true; } catch { return false; } + try { execSync("which brew", { stdio: "ignore", windowsHide: true, env: { ...process.env, PATH: EXTENDED_PATH } }); return true; } catch { return false; } } function isDaemonRunning() { @@ -14,6 +14,7 @@ function isDaemonRunning() { // Use custom socket + --json; exit 0 even when not logged in execSync(`tailscale --socket ${TAILSCALE_SOCKET} status --json`, { stdio: "ignore", + windowsHide: true, env: { ...process.env, PATH: EXTENDED_PATH }, timeout: 3000 }); @@ -21,7 +22,7 @@ function isDaemonRunning() { } catch { // Fallback: check if tailscaled process is alive try { - execSync("pgrep -x tailscaled", { stdio: "ignore", timeout: 2000 }); + execSync("pgrep -x tailscaled", { stdio: "ignore", windowsHide: true, timeout: 2000 }); return true; } catch { return false; } } diff --git a/src/app/api/tunnel/tailscale-install/route.js b/src/app/api/tunnel/tailscale-install/route.js index 9ec2015..e16e687 100644 --- a/src/app/api/tunnel/tailscale-install/route.js +++ b/src/app/api/tunnel/tailscale-install/route.js @@ -12,7 +12,7 @@ initDbHooks(getSettings, updateSettings); const EXTENDED_PATH = `/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:${process.env.PATH || ""}`; function hasBrew() { - try { execSync("which brew", { stdio: "ignore", env: { ...process.env, PATH: EXTENDED_PATH } }); return true; } catch { return false; } + try { execSync("which brew", { stdio: "ignore", windowsHide: true, env: { ...process.env, PATH: EXTENDED_PATH } }); return true; } catch { return false; } } export async function POST(request) { diff --git a/src/lib/tunnel/cloudflared.js b/src/lib/tunnel/cloudflared.js index cfe7e75..a17b6be 100644 --- a/src/lib/tunnel/cloudflared.js +++ b/src/lib/tunnel/cloudflared.js @@ -103,7 +103,7 @@ export async function ensureCloudflared() { await downloadFile(url, downloadDest); if (isArchive) { - execSync(`tar -xzf "${downloadDest}" -C "${BIN_DIR}"`, { stdio: "pipe" }); + execSync(`tar -xzf "${downloadDest}" -C "${BIN_DIR}"`, { stdio: "pipe", windowsHide: true }); fs.unlinkSync(downloadDest); } @@ -312,7 +312,7 @@ export function killCloudflared() { // Kill any remaining cloudflared processes try { - execSync("pkill -f cloudflared 2>/dev/null || true", { stdio: "ignore" }); + execSync("pkill -f cloudflared 2>/dev/null || true", { stdio: "ignore", windowsHide: true }); } catch (e) { /* ignore */ } } diff --git a/src/lib/tunnel/tailscale.js b/src/lib/tunnel/tailscale.js index 3b442d5..e6d3697 100644 --- a/src/lib/tunnel/tailscale.js +++ b/src/lib/tunnel/tailscale.js @@ -19,7 +19,7 @@ const SOCKET_FLAG = IS_WINDOWS ? [] : ["--socket", TAILSCALE_SOCKET]; // Prefer system tailscale, fallback to local bin function getTailscaleBin() { try { - const systemPath = execSync("which tailscale 2>/dev/null || where tailscale 2>nul", { encoding: "utf8" }).trim(); + const systemPath = execSync("which tailscale 2>/dev/null || where tailscale 2>nul", { encoding: "utf8", windowsHide: true }).trim(); if (systemPath) return systemPath; } catch (e) { /* not in PATH */ } if (fs.existsSync(TAILSCALE_BIN)) return TAILSCALE_BIN; @@ -41,6 +41,7 @@ export function isTailscaleLoggedIn() { try { const out = execSync(`"${bin}" ${SOCKET_FLAG.join(" ")} status --json`, { encoding: "utf8", + windowsHide: true, env: { ...process.env, PATH: EXTENDED_PATH }, timeout: 5000 }); @@ -56,7 +57,7 @@ export function isTailscaleRunning() { const bin = getTailscaleBin(); if (!bin) return false; try { - const out = execSync(`"${bin}" ${SOCKET_FLAG.join(" ")} funnel status --json 2>/dev/null`, { encoding: "utf8" }); + const out = execSync(`"${bin}" ${SOCKET_FLAG.join(" ")} funnel status --json 2>/dev/null`, { encoding: "utf8", windowsHide: true }); const json = JSON.parse(out); return Object.keys(json.AllowFunnel || {}).length > 0; } catch (e) { @@ -69,7 +70,7 @@ export function getTailscaleFunnelUrl(port) { const bin = getTailscaleBin(); if (!bin) return null; try { - const out = execSync(`"${bin}" ${SOCKET_FLAG.join(" ")} status --json`, { encoding: "utf8" }); + const out = execSync(`"${bin}" ${SOCKET_FLAG.join(" ")} status --json`, { encoding: "utf8", windowsHide: true }); const json = JSON.parse(out); const dnsName = json.Self?.DNSName?.replace(/\.$/, ""); if (dnsName) return `https://${dnsName}`; @@ -102,7 +103,7 @@ export async function installTailscale(sudoPassword, hostname, onProgress) { const EXTENDED_PATH = `/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:${process.env.PATH || ""}`; function hasBrew() { - try { execSync("which brew", { stdio: "ignore", env: { ...process.env, PATH: EXTENDED_PATH } }); return true; } catch { return false; } + try { execSync("which brew", { stdio: "ignore", windowsHide: true, env: { ...process.env, PATH: EXTENDED_PATH } }); return true; } catch { return false; } } async function installTailscaleMac(sudoPassword, log) { @@ -111,6 +112,7 @@ async function installTailscaleMac(sudoPassword, log) { await new Promise((resolve, reject) => { const child = spawn("brew", ["install", "tailscale"], { stdio: ["ignore", "pipe", "pipe"], + windowsHide: true, env: { ...process.env, PATH: EXTENDED_PATH } }); child.stdout.on("data", (d) => { @@ -137,7 +139,8 @@ async function installTailscaleMac(sudoPassword, log) { log("Downloading Tailscale package..."); await new Promise((resolve, reject) => { const child = spawn("curl", ["-fL", "--progress-bar", pkgUrl, "-o", pkgPath], { - stdio: ["ignore", "pipe", "pipe"] + stdio: ["ignore", "pipe", "pipe"], + windowsHide: true }); child.stderr.on("data", (d) => { const line = d.toString().trim(); @@ -153,7 +156,8 @@ async function installTailscaleMac(sudoPassword, log) { log("Installing package..."); await new Promise((resolve, reject) => { const child = spawn("sudo", ["-S", "installer", "-pkg", pkgPath, "-target", "/"], { - stdio: ["pipe", "pipe", "pipe"] + stdio: ["pipe", "pipe", "pipe"], + windowsHide: true }); let stderr = ""; child.stderr.on("data", (d) => { stderr += d.toString(); }); @@ -162,7 +166,7 @@ async function installTailscaleMac(sudoPassword, log) { if (line) log(line); }); child.on("close", (c) => { - try { execSync(`rm -f ${pkgPath}`, { stdio: "ignore" }); } catch { /* ignore */ } + try { execSync(`rm -f ${pkgPath}`, { stdio: "ignore", windowsHide: true }); } catch { /* ignore */ } if (c === 0) resolve(); else { const msg = (stderr.includes("incorrect password") || stderr.includes("Sorry")) @@ -181,7 +185,8 @@ async function installTailscaleLinux(sudoPassword, log) { log("Downloading install script..."); return new Promise((resolve, reject) => { const curlChild = spawn("curl", ["-fsSL", "https://tailscale.com/install.sh"], { - stdio: ["ignore", "pipe", "pipe"] + stdio: ["ignore", "pipe", "pipe"], + windowsHide: true }); let scriptContent = ""; let curlErr = ""; @@ -190,7 +195,7 @@ async function installTailscaleLinux(sudoPassword, log) { curlChild.on("exit", (code) => { if (code !== 0) return reject(new Error(`Failed to download install script: ${curlErr}`)); log("Running install script..."); - const child = spawn("sudo", ["-S", "sh"], { stdio: ["pipe", "pipe", "pipe"] }); + const child = spawn("sudo", ["-S", "sh"], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true }); let stderr = ""; child.stdout.on("data", (d) => { const line = d.toString().trim(); @@ -225,7 +230,7 @@ async function installTailscaleWindows(log) { const child = spawn("powershell", [ "-NoProfile", "-NonInteractive", "-Command", `Invoke-WebRequest -Uri '${msiUrl}' -OutFile '${msiPath}'` - ], { stdio: ["ignore", "pipe", "pipe"] }); + ], { stdio: ["ignore", "pipe", "pipe"], windowsHide: true }); child.stdout.on("data", (d) => { const l = d.toString().trim(); if (l) log(l); }); child.stderr.on("data", (d) => { const l = d.toString().trim(); if (l) log(l); }); child.on("close", (c) => c === 0 ? resolve() : reject(new Error("Download failed"))); @@ -236,7 +241,8 @@ async function installTailscaleWindows(log) { log("Installing Tailscale (UAC prompt may appear)..."); await new Promise((resolve, reject) => { const child = spawn("msiexec", ["/i", msiPath, "/quiet", "/norestart"], { - stdio: ["ignore", "pipe", "pipe"] + stdio: ["ignore", "pipe", "pipe"], + windowsHide: true }); child.stdout.on("data", (d) => { const l = d.toString().trim(); if (l) log(l); }); child.stderr.on("data", (d) => { const l = d.toString().trim(); if (l) log(l); }); @@ -259,6 +265,7 @@ export async function startDaemonWithPassword(sudoPassword) { const bin = getTailscaleBin() || "tailscale"; execSync(`"${bin}" ${SOCKET_FLAG.join(" ")} status --json`, { stdio: "ignore", + windowsHide: true, env: { ...process.env, PATH: EXTENDED_PATH }, timeout: 3000 }); @@ -307,7 +314,8 @@ export function startLogin(hostname) { if (hostname) args.push(`--hostname=${hostname}`); const child = spawn(bin, args, { stdio: ["ignore", "pipe", "pipe"], - detached: true + detached: true, + windowsHide: true }); let resolved = false; @@ -368,11 +376,12 @@ export async function startFunnel(port) { if (!bin) throw new Error("Tailscale not installed"); // Reset any existing funnel - try { execSync(`"${bin}" ${SOCKET_FLAG.join(" ")} funnel --bg reset`, { stdio: "ignore" }); } catch (e) { /* ignore */ } + try { execSync(`"${bin}" ${SOCKET_FLAG.join(" ")} funnel --bg reset`, { stdio: "ignore", windowsHide: true }); } catch (e) { /* ignore */ } return new Promise((resolve, reject) => { const child = spawn(bin, tsArgs("funnel", "--bg", `${port}`), { - stdio: ["ignore", "pipe", "pipe"] + stdio: ["ignore", "pipe", "pipe"], + windowsHide: true }); let resolved = false; @@ -442,16 +451,16 @@ export async function startFunnel(port) { export function stopFunnel() { const bin = getTailscaleBin(); if (!bin) return; - try { execSync(`"${bin}" ${SOCKET_FLAG.join(" ")} funnel --bg reset`, { stdio: "ignore" }); } catch (e) { /* ignore */ } + try { execSync(`"${bin}" ${SOCKET_FLAG.join(" ")} funnel --bg reset`, { stdio: "ignore", windowsHide: true }); } catch (e) { /* ignore */ } } /** Kill tailscaled daemon (runs as root, needs sudo) */ export async function stopDaemon(sudoPassword) { // Try non-sudo first - try { execSync("pkill -x tailscaled", { stdio: "ignore", timeout: 3000 }); } catch { /* ignore */ } + try { execSync("pkill -x tailscaled", { stdio: "ignore", windowsHide: true, timeout: 3000 }); } catch { /* ignore */ } // Check if still alive - try { execSync("pgrep -x tailscaled", { stdio: "ignore", timeout: 2000 }); } catch { return; } // Dead, done + try { execSync("pgrep -x tailscaled", { stdio: "ignore", windowsHide: true, timeout: 2000 }); } catch { return; } // Dead, done // Kill with sudo password if (!IS_WINDOWS) { diff --git a/src/mitm/cert/install.js b/src/mitm/cert/install.js index e5e4d0c..115a692 100644 --- a/src/mitm/cert/install.js +++ b/src/mitm/cert/install.js @@ -29,10 +29,10 @@ function checkCertInstalledMac(certPath) { try { const fingerprint = getCertFingerprint(certPath).replace(/:/g, ""); // security verify-cert returns 0 only if cert is trusted by system policy - exec(`security verify-cert -c "${certPath}" -p ssl -k /Library/Keychains/System.keychain 2>/dev/null`, (error) => { + exec(`security verify-cert -c "${certPath}" -p ssl -k /Library/Keychains/System.keychain 2>/dev/null`, { windowsHide: true }, (error) => { if (!error) return resolve(true); // Fallback: check if fingerprint appears in System keychain with trust - exec(`security dump-trust-settings -d 2>/dev/null | grep -i "${fingerprint}"`, (err2, stdout2) => { + exec(`security dump-trust-settings -d 2>/dev/null | grep -i "${fingerprint}"`, { windowsHide: true }, (err2, stdout2) => { resolve(!err2 && !!stdout2?.trim()); }); }); diff --git a/src/mitm/dns/dnsConfig.js b/src/mitm/dns/dnsConfig.js index 408755f..801adf6 100644 --- a/src/mitm/dns/dnsConfig.js +++ b/src/mitm/dns/dnsConfig.js @@ -63,7 +63,7 @@ function executeElevatedPowerShell(psScriptPath, timeoutMs = 30000) { function isSudoAvailable() { if (IS_WIN) return false; try { - execSync("command -v sudo", { stdio: "ignore" }); + execSync("command -v sudo", { stdio: "ignore", windowsHide: true }); return true; } catch { return false; @@ -78,8 +78,8 @@ function execWithPassword(command, password) { return new Promise((resolve, reject) => { const useSudo = isSudoAvailable(); const child = useSudo - ? spawn("sudo", ["-S", "sh", "-c", command], { stdio: ["pipe", "pipe", "pipe"] }) - : spawn("sh", ["-c", command], { stdio: ["ignore", "pipe", "pipe"] }); + ? spawn("sudo", ["-S", "sh", "-c", command], { stdio: ["pipe", "pipe", "pipe"], windowsHide: true }) + : spawn("sh", ["-c", command], { stdio: ["ignore", "pipe", "pipe"], windowsHide: true }); let stdout = ""; let stderr = ""; diff --git a/src/mitm/manager.js b/src/mitm/manager.js index 897145a..865cf6c 100644 --- a/src/mitm/manager.js +++ b/src/mitm/manager.js @@ -76,7 +76,7 @@ function getProcessUsingPort443() { if (processMatch) return processMatch[1].replace(".exe", ""); } } else { - const result = execSync("lsof -i :443", { encoding: "utf8" }); + const result = execSync("lsof -i :443", { encoding: "utf8", windowsHide: true }); const lines = result.trim().split("\n"); if (lines.length > 1) return lines[1].split(/\s+/)[0]; } @@ -110,9 +110,9 @@ function killProcess(pid, force = false, sudoPassword = null) { const cmd = `pkill -${sig} -P ${pid} 2>/dev/null; kill -${sig} ${pid} 2>/dev/null`; if (sudoPassword) { const { execWithPassword } = require("./dns/dnsConfig"); - execWithPassword(cmd, sudoPassword).catch(() => exec(cmd, () => { })); + execWithPassword(cmd, sudoPassword).catch(() => exec(cmd, { windowsHide: true }, () => { })); } else { - exec(cmd, () => { }); + exec(cmd, { windowsHide: true }, () => { }); } } } @@ -216,7 +216,7 @@ function getPort443Owner(sudoPassword) { }); }); } else { - exec(`ps aux | grep "[s]erver.js"`, (err, stdout) => { + exec(`ps aux | grep "[s]erver.js"`, { windowsHide: true }, (err, stdout) => { if (!stdout?.trim()) return resolve(null); for (const line of stdout.split("\n")) { const parts = line.trim().split(/\s+/); @@ -252,7 +252,7 @@ async function killLeftoverMitm(sudoPassword) { const { execWithPassword } = require("./dns/dnsConfig"); await execWithPassword(`pkill -SIGKILL -f "${escaped}" 2>/dev/null || true`, sudoPassword).catch(() => { }); } else { - exec(`pkill -SIGKILL -f "${escaped}" 2>/dev/null || true`, () => { }); + exec(`pkill -SIGKILL -f "${escaped}" 2>/dev/null || true`, { windowsHide: true }, () => { }); } await new Promise(r => setTimeout(r, 500)); } catch { /* ignore */ } @@ -489,7 +489,7 @@ async function startServer(apiKey, sudoPassword) { ].join(" "); serverProcess = spawn( "sudo", ["-S", "-E", "sh", "-c", inlineCmd], - { detached: false, stdio: ["pipe", "pipe", "pipe"] } + { detached: false, windowsHide: true, stdio: ["pipe", "pipe", "pipe"] } ); serverProcess.stdin.write(`${sudoPassword}\n`); serverProcess.stdin.end(); @@ -497,6 +497,7 @@ async function startServer(apiKey, sudoPassword) { // Docker/minimal images: no sudo — same as Windows-style direct spawn serverProcess = spawn(process.execPath, [SERVER_PATH], { detached: false, + windowsHide: true, stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, diff --git a/src/mitm/server.js b/src/mitm/server.js index 5cd40d8..953cd8d 100644 --- a/src/mitm/server.js +++ b/src/mitm/server.js @@ -222,7 +222,7 @@ const server = https.createServer(sslOptions, async (req, res) => { // Kill any process occupying LOCAL_PORT before binding function killPort(port) { try { - const pids = execSync(`lsof -ti :${port}`, { encoding: "utf-8" }).trim(); + const pids = execSync(`lsof -ti :${port}`, { encoding: "utf-8", windowsHide: true }).trim(); if (!pids) return; const pidList = pids.split("\n"); pidList.forEach(pid => {