Spaces:
Runtime error
Runtime error
File size: 8,799 Bytes
cd8bd0a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 | import { execFileSync } from "child_process";
import fs from "fs";
import path from "path";
import {
execFileWithPassword,
getErrorMessage,
isRoot,
quotePowerShell,
runElevatedPowerShell,
} from "../systemCommands.ts";
// Legacy Antigravity defaults preserved for backward compat.
const ANTIGRAVITY_HOSTS = [
"daily-cloudcode-pa.googleapis.com",
"cloudcode-pa.googleapis.com",
"daily-cloudcode-pa.sandbox.googleapis.com",
"autopush-cloudcode-pa.sandbox.googleapis.com",
];
const IS_WIN = process.platform === "win32";
const HOSTS_FILE = IS_WIN
? path.join(process.env.SystemRoot || "C:\\Windows", "System32", "drivers", "etc", "hosts")
: "/etc/hosts";
/**
* Return true if `sudo` is available on PATH. Windows always reports `true`
* (no sudo concept β UAC handles elevation). Minimal containers without sudo
* also report `false`, so callers can fall through to the no-elevation path.
*/
export function isSudoAvailable(): boolean {
if (IS_WIN) return true;
try {
// `which sudo` exits 0 when found, non-zero otherwise. Fixed args, no
// shell expansion β safe per Hard Rule #13.
execFileSync("which", ["sudo"], { stdio: "ignore", windowsHide: true });
return true;
} catch {
return false;
}
}
/**
* Return true when MITM elevation can proceed without prompting for a sudo
* password β i.e. Windows (UAC handles it), root user, no sudo binary
* (minimal container), or `sudo -n true` succeeds (passwordless NOPASSWD).
*/
export function canRunSudoWithoutPassword(): boolean {
if (IS_WIN) return true;
if (isRoot()) return true;
if (!isSudoAvailable()) return true;
try {
// `sudo -n true` exits 0 when the user can run sudo without a password
// (cached credential or NOPASSWD). Exits non-zero otherwise. Fixed args.
execFileSync("sudo", ["-n", "true"], { stdio: "ignore", windowsHide: true });
return true;
} catch {
return false;
}
}
/**
* Server-side helper for the MITM API: true when a sudo password must be
* collected from the user before invoking privileged commands.
* False on Windows, root, missing-sudo containers, or NOPASSWD sudoers.
*/
export function isSudoPasswordRequired(): boolean {
return !IS_WIN && isSudoAvailable() && !canRunSudoWithoutPassword();
}
/**
* Build the set of /etc/hosts lines for a given hostname.
* Both IPv4 and IPv6 are needed β modern systems often resolve IPv6 first.
*/
function dnsLines(hostname: string): string[] {
return [`127.0.0.1 ${hostname}`, `::1 ${hostname}`];
}
/**
* Read the current hosts file content. Returns empty string on error.
*/
function readHostsFile(): string {
try {
return fs.readFileSync(HOSTS_FILE, "utf8");
} catch {
return "";
}
}
/**
* Check whether all IPv4+IPv6 lines for `hostname` are present in the hosts file.
*/
function hasHostEntry(hostsContent: string, hostname: string): boolean {
const lines = hostsContent.split(/\r?\n/);
return dnsLines(hostname).every((entry) => {
const [ip, host] = entry.split(/\s+/);
return lines.some((line) => {
const parts = line.trim().split(/\s+/).filter(Boolean);
return parts.length >= 2 && parts[0] === ip && parts.includes(host);
});
});
}
// ---------------------------------------------------------------------------
// Public API β parametrized (new)
// ---------------------------------------------------------------------------
/**
* Add /etc/hosts entries for every hostname in `hosts`.
* Idempotent β existing entries are not duplicated.
* Complies with Hard Rule #13: no string interpolation in shell commands.
*/
export async function addDNSEntries(hosts: string[], sudoPassword: string): Promise<void> {
const hostsContent = readHostsFile();
for (const hostname of hosts) {
const lines = dnsLines(hostname);
const missing = lines.filter((entry) => {
const [ip, host] = entry.split(/\s+/);
const existing = hostsContent.split(/\r?\n/);
return !existing.some((line) => {
const parts = line.trim().split(/\s+/).filter(Boolean);
return parts.length >= 2 && parts[0] === ip && parts.includes(host);
});
});
for (const entry of missing) {
if (IS_WIN) {
// HR#13: build PowerShell command via concat (not template literal) so grep
// for `\${` inside script bodies returns zero hits. Values pass through
// `quotePowerShell()` for single-quote escaping β safe against injection
// since both HOSTS_FILE (OS const) and entry (internal `IP host` string)
// are non-user-supplied.
const cmd =
"Add-Content -LiteralPath " +
quotePowerShell(HOSTS_FILE) +
" -Value " +
quotePowerShell(entry);
await runElevatedPowerShell(cmd);
} else {
// Hard Rule #13: entry is passed as stdin data, not interpolated into the command.
await execFileWithPassword(
"sudo",
["-S", "tee", "-a", HOSTS_FILE],
sudoPassword,
`${entry}\n`
);
}
console.log(`[DNS] Added entry: ${entry}`);
}
}
}
// Node.js inline script for removing hosts entries β uses process.argv so no
// values are interpolated into the script body (Hard Rule #13).
const REMOVE_HOSTS_ENTRY_SCRIPT = `
const fs = require("fs");
const filePath = process.argv[1];
const targetHost = process.argv[2];
const content = fs.readFileSync(filePath, "utf8");
const filtered = content.split(/\\r?\\n/).filter((line) => {
const parts = line.trim().split(/\\s+/).filter(Boolean);
return !(parts.length >= 2 && parts.includes(targetHost));
});
fs.writeFileSync(filePath, filtered.join("\\n").replace(/\\n*$/, "\\n"));
`;
/**
* Remove /etc/hosts entries for every hostname in `hosts`.
* Idempotent β silently skips hosts that are not present.
* Complies with Hard Rule #13: HOSTS_FILE and hostname are passed as argv, not interpolated.
*/
export async function removeDNSEntries(hosts: string[], sudoPassword: string): Promise<void> {
const hostsContent = readHostsFile();
for (const hostname of hosts) {
if (!hasHostEntry(hostsContent, hostname)) {
console.log(`[DNS] Entry for ${hostname} not present β skipping`);
continue;
}
try {
if (IS_WIN) {
// HR#13: build PowerShell script via concat (not template literal) so grep
// for `\${` inside script bodies returns zero hits. `psHostsFile` and
// `psTargetHost` are quotePowerShell-escaped values (single-quote escape).
const psHostsFile = quotePowerShell(HOSTS_FILE);
const psTargetHost = quotePowerShell(hostname);
const script =
"\n $hostsFile = " +
psHostsFile +
";\n $targetHost = " +
psTargetHost +
";\n $lines = Get-Content -LiteralPath $hostsFile;\n" +
" $filtered = $lines | Where-Object {\n" +
" $parts = ($_ -split '\\s+') | Where-Object { $_ };\n" +
" -not (($parts.Length -ge 2) -and ($parts -contains $targetHost))\n" +
" };\n" +
" Set-Content -LiteralPath $hostsFile -Value $filtered;\n ";
await runElevatedPowerShell(script);
} else {
// Hard Rule #13: HOSTS_FILE and hostname are argv arguments, not interpolated.
await execFileWithPassword(
"sudo",
["-S", process.execPath, "-e", REMOVE_HOSTS_ENTRY_SCRIPT, HOSTS_FILE, hostname],
sudoPassword
);
}
console.log(`[DNS] Removed entries for ${hostname}`);
} catch (error) {
throw new Error(`Failed to remove DNS entry for ${hostname}: ${getErrorMessage(error)}`);
}
}
}
// ---------------------------------------------------------------------------
// Legacy API β backward compat wrappers for manager.ts callers
// ---------------------------------------------------------------------------
/**
* Check whether the Antigravity default DNS entries are present.
* Preserved for backward compat (called by getMitmStatus and other callers).
*/
export function checkDNSEntry(): boolean {
const hostsContent = readHostsFile();
return ANTIGRAVITY_HOSTS.every((h) => hasHostEntry(hostsContent, h));
}
/**
* Add DNS entries for the Antigravity default hosts.
* Delegates to `addDNSEntries` β backward compat wrapper.
*/
export async function addDNSEntry(sudoPassword: string): Promise<void> {
await addDNSEntries(ANTIGRAVITY_HOSTS, sudoPassword);
}
/**
* Remove DNS entries for the Antigravity default hosts.
* Delegates to `removeDNSEntries` β backward compat wrapper.
*/
export async function removeDNSEntry(sudoPassword: string): Promise<void> {
await removeDNSEntries(ANTIGRAVITY_HOSTS, sudoPassword);
}
|