mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 20:00:47 +00:00
feat(antigravity): integrate Antigravity tool with MITM support and update CLI tools
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
|
||||
const TARGET_HOST = "daily-cloudcode-pa.googleapis.com";
|
||||
|
||||
/**
|
||||
* Generate self-signed SSL certificate using selfsigned (pure JS, no openssl needed)
|
||||
*/
|
||||
async function generateCert() {
|
||||
const certDir = path.join(os.homedir(), ".9router", "mitm");
|
||||
const keyPath = path.join(certDir, "server.key");
|
||||
const certPath = path.join(certDir, "server.crt");
|
||||
|
||||
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
||||
console.log("✅ SSL certificate already exists");
|
||||
return { key: keyPath, cert: certPath };
|
||||
}
|
||||
|
||||
if (!fs.existsSync(certDir)) {
|
||||
fs.mkdirSync(certDir, { recursive: true });
|
||||
}
|
||||
|
||||
const selfsigned = require("selfsigned");
|
||||
const attrs = [{ name: "commonName", value: TARGET_HOST }];
|
||||
const notAfter = new Date();
|
||||
notAfter.setFullYear(notAfter.getFullYear() + 1);
|
||||
const pems = await selfsigned.generate(attrs, {
|
||||
keySize: 2048,
|
||||
algorithm: "sha256",
|
||||
notAfterDate: notAfter,
|
||||
extensions: [
|
||||
{ name: "subjectAltName", altNames: [{ type: 2, value: TARGET_HOST }] }
|
||||
]
|
||||
});
|
||||
|
||||
fs.writeFileSync(keyPath, pems.private);
|
||||
fs.writeFileSync(certPath, pems.cert);
|
||||
|
||||
console.log(`✅ Generated SSL certificate for ${TARGET_HOST}`);
|
||||
return { key: keyPath, cert: certPath };
|
||||
}
|
||||
|
||||
module.exports = { generateCert };
|
||||
@@ -0,0 +1,136 @@
|
||||
const fs = require("fs");
|
||||
const crypto = require("crypto");
|
||||
const { exec } = require("child_process");
|
||||
const { execWithPassword } = require("../dns/dnsConfig.js");
|
||||
|
||||
const IS_WIN = process.platform === "win32";
|
||||
|
||||
// Get SHA1 fingerprint from cert file using Node.js crypto
|
||||
function getCertFingerprint(certPath) {
|
||||
const pem = fs.readFileSync(certPath, "utf-8");
|
||||
const der = Buffer.from(pem.replace(/-----[^-]+-----/g, "").replace(/\s/g, ""), "base64");
|
||||
return crypto.createHash("sha1").update(der).digest("hex").toUpperCase().match(/.{2}/g).join(":");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if certificate is already installed in system store
|
||||
*/
|
||||
async function checkCertInstalled(certPath) {
|
||||
if (IS_WIN) {
|
||||
return checkCertInstalledWindows(certPath);
|
||||
}
|
||||
return checkCertInstalledMac(certPath);
|
||||
}
|
||||
|
||||
function checkCertInstalledMac(certPath) {
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const fingerprint = getCertFingerprint(certPath);
|
||||
exec(`security find-certificate -a -Z /Library/Keychains/System.keychain | grep -i "${fingerprint}"`, (error) => {
|
||||
resolve(!error);
|
||||
});
|
||||
} catch {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function checkCertInstalledWindows(certPath) {
|
||||
return new Promise((resolve) => {
|
||||
// Check Root store for our cert by subject name
|
||||
exec("certutil -store Root daily-cloudcode-pa.googleapis.com", (error) => {
|
||||
resolve(!error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Install SSL certificate to system trust store
|
||||
*/
|
||||
async function installCert(sudoPassword, certPath) {
|
||||
if (!fs.existsSync(certPath)) {
|
||||
throw new Error(`Certificate file not found: ${certPath}`);
|
||||
}
|
||||
|
||||
const isInstalled = await checkCertInstalled(certPath);
|
||||
if (isInstalled) {
|
||||
console.log("✅ Certificate already installed");
|
||||
return;
|
||||
}
|
||||
|
||||
if (IS_WIN) {
|
||||
await installCertWindows(certPath);
|
||||
} else {
|
||||
await installCertMac(sudoPassword, certPath);
|
||||
}
|
||||
}
|
||||
|
||||
async function installCertMac(sudoPassword, certPath) {
|
||||
const command = `sudo -S security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${certPath}"`;
|
||||
try {
|
||||
await execWithPassword(command, sudoPassword);
|
||||
console.log(`✅ Installed certificate to system keychain: ${certPath}`);
|
||||
} catch (error) {
|
||||
const msg = error.message?.includes("canceled") ? "User canceled authorization" : "Certificate install failed";
|
||||
throw new Error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
async function installCertWindows(certPath) {
|
||||
// Use PowerShell elevated to add cert to Root store
|
||||
const psCommand = `Start-Process certutil -ArgumentList '-addstore','Root','${certPath.replace(/'/g, "''")}' -Verb RunAs -Wait`;
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`powershell -Command "${psCommand}"`, (error) => {
|
||||
if (error) {
|
||||
reject(new Error(`Failed to install certificate: ${error.message}`));
|
||||
} else {
|
||||
console.log(`✅ Installed certificate to Windows Root store`);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall SSL certificate from system store
|
||||
*/
|
||||
async function uninstallCert(sudoPassword, certPath) {
|
||||
const isInstalled = await checkCertInstalled(certPath);
|
||||
if (!isInstalled) {
|
||||
console.log("Certificate not found in system store");
|
||||
return;
|
||||
}
|
||||
|
||||
if (IS_WIN) {
|
||||
await uninstallCertWindows();
|
||||
} else {
|
||||
await uninstallCertMac(sudoPassword, certPath);
|
||||
}
|
||||
}
|
||||
|
||||
async function uninstallCertMac(sudoPassword, certPath) {
|
||||
const fingerprint = getCertFingerprint(certPath).replace(/:/g, "");
|
||||
const command = `sudo -S security delete-certificate -Z "${fingerprint}" /Library/Keychains/System.keychain`;
|
||||
try {
|
||||
await execWithPassword(command, sudoPassword);
|
||||
console.log("✅ Uninstalled certificate from system keychain");
|
||||
} catch (err) {
|
||||
throw new Error("Failed to uninstall certificate");
|
||||
}
|
||||
}
|
||||
|
||||
async function uninstallCertWindows() {
|
||||
const psCommand = `Start-Process certutil -ArgumentList '-delstore','Root','daily-cloudcode-pa.googleapis.com' -Verb RunAs -Wait`;
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(`powershell -Command "${psCommand}"`, (error) => {
|
||||
if (error) {
|
||||
reject(new Error(`Failed to uninstall certificate: ${error.message}`));
|
||||
} else {
|
||||
console.log("✅ Uninstalled certificate from Windows Root store");
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { installCert, uninstallCert, checkCertInstalled };
|
||||
@@ -0,0 +1,111 @@
|
||||
const { exec } = require("child_process");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const TARGET_HOST = "daily-cloudcode-pa.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";
|
||||
|
||||
/**
|
||||
* Execute command with sudo password via stdin (macOS/Linux only)
|
||||
*/
|
||||
function execWithPassword(command, password) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(new Error(`Command failed: ${error.message}\n${stderr}`));
|
||||
} else {
|
||||
resolve(stdout);
|
||||
}
|
||||
});
|
||||
child.stdin.write(`${password}\n`);
|
||||
child.stdin.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute elevated command on Windows via PowerShell RunAs
|
||||
*/
|
||||
function execElevatedWindows(command) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const psCommand = `Start-Process cmd -ArgumentList '/c','${command.replace(/'/g, "''")}' -Verb RunAs -Wait`;
|
||||
exec(`powershell -Command "${psCommand}"`, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(new Error(`Elevated command failed: ${error.message}\n${stderr}`));
|
||||
} else {
|
||||
resolve(stdout);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if DNS entry already exists
|
||||
*/
|
||||
function checkDNSEntry() {
|
||||
try {
|
||||
const hostsContent = fs.readFileSync(HOSTS_FILE, "utf8");
|
||||
return hostsContent.includes(TARGET_HOST);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add DNS entry to hosts file
|
||||
*/
|
||||
async function addDNSEntry(sudoPassword) {
|
||||
if (checkDNSEntry()) {
|
||||
console.log(`DNS entry for ${TARGET_HOST} already exists`);
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = `127.0.0.1 ${TARGET_HOST}`;
|
||||
|
||||
try {
|
||||
if (IS_WIN) {
|
||||
// Windows: use elevated echo >> hosts
|
||||
await execElevatedWindows(`echo ${entry} >> "${HOSTS_FILE}"`);
|
||||
} else {
|
||||
const command = `echo "${entry}" | sudo -S tee -a ${HOSTS_FILE} > /dev/null`;
|
||||
await execWithPassword(command, sudoPassword);
|
||||
}
|
||||
console.log(`✅ Added DNS entry: ${entry}`);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to add DNS entry: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove DNS entry from hosts file
|
||||
*/
|
||||
async function removeDNSEntry(sudoPassword) {
|
||||
if (!checkDNSEntry()) {
|
||||
console.log(`DNS entry for ${TARGET_HOST} does not exist`);
|
||||
return;
|
||||
}
|
||||
|
||||
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`;
|
||||
await new Promise((resolve, reject) => {
|
||||
exec(`powershell -Command "${psCommand}"`, (error) => {
|
||||
if (error) reject(new Error(`Failed to remove DNS entry: ${error.message}`));
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const command = `sudo -S sed -i '' '/${TARGET_HOST}/d' ${HOSTS_FILE}`;
|
||||
await execWithPassword(command, sudoPassword);
|
||||
}
|
||||
console.log(`✅ Removed DNS entry for ${TARGET_HOST}`);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to remove DNS entry: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { addDNSEntry, removeDNSEntry, execWithPassword, checkDNSEntry };
|
||||
@@ -0,0 +1,227 @@
|
||||
const { spawn } = require("child_process");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const { addDNSEntry, removeDNSEntry } = require("./dns/dnsConfig");
|
||||
const { generateCert } = require("./cert/generate");
|
||||
const { installCert } = require("./cert/install");
|
||||
|
||||
// Store server process
|
||||
let serverProcess = null;
|
||||
let serverPid = null;
|
||||
// Persist across Next.js hot reloads
|
||||
function getCachedPassword() { return globalThis.__mitmSudoPassword || null; }
|
||||
function setCachedPassword(pwd) { globalThis.__mitmSudoPassword = pwd; }
|
||||
|
||||
// server.js is in same directory as this file
|
||||
const PID_FILE = path.join(os.homedir(), ".9router", "mitm", ".mitm.pid");
|
||||
|
||||
// Check if a PID is alive
|
||||
function isProcessAlive(pid) {
|
||||
try {
|
||||
process.kill(pid, 0);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get MITM status
|
||||
*/
|
||||
async function getMitmStatus() {
|
||||
// Check in-memory process first, then fallback to PID file
|
||||
let running = serverProcess !== null && !serverProcess.killed;
|
||||
let pid = serverPid;
|
||||
|
||||
if (!running) {
|
||||
try {
|
||||
if (fs.existsSync(PID_FILE)) {
|
||||
const savedPid = parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10);
|
||||
if (savedPid && isProcessAlive(savedPid)) {
|
||||
running = true;
|
||||
pid = savedPid;
|
||||
} else {
|
||||
// Stale PID file, clean up
|
||||
fs.unlinkSync(PID_FILE);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
// Check DNS configuration
|
||||
let dnsConfigured = false;
|
||||
try {
|
||||
const hostsContent = fs.readFileSync("/etc/hosts", "utf-8");
|
||||
dnsConfigured = hostsContent.includes("daily-cloudcode-pa.googleapis.com");
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
// Check cert
|
||||
const certDir = path.join(os.homedir(), ".9router", "mitm");
|
||||
const certExists = fs.existsSync(path.join(certDir, "server.crt"));
|
||||
|
||||
return { running, pid, dnsConfigured, certExists };
|
||||
}
|
||||
|
||||
/**
|
||||
* Start MITM proxy
|
||||
* @param {string} apiKey - 9Router API key
|
||||
* @param {string} sudoPassword - Sudo password for DNS/cert operations
|
||||
*/
|
||||
async function startMitm(apiKey, sudoPassword) {
|
||||
// Check if already running
|
||||
if (serverProcess && !serverProcess.killed) {
|
||||
throw new Error("MITM proxy is already running");
|
||||
}
|
||||
|
||||
// 1. Generate SSL certificate if not exists
|
||||
const certPath = path.join(os.homedir(), ".9router", "mitm", "server.crt");
|
||||
if (!fs.existsSync(certPath)) {
|
||||
console.log("Generating SSL certificate...");
|
||||
await generateCert();
|
||||
}
|
||||
|
||||
// 2. Install certificate to system keychain
|
||||
await installCert(sudoPassword, certPath);
|
||||
|
||||
// 3. Add DNS entry
|
||||
console.log("Adding DNS entry...");
|
||||
await addDNSEntry(sudoPassword);
|
||||
|
||||
// 4. Start MITM server
|
||||
console.log("Starting MITM server...");
|
||||
const serverPath = path.join(process.cwd(), "src/mitm/server.js");
|
||||
serverProcess = spawn("node", [serverPath], {
|
||||
env: {
|
||||
...process.env,
|
||||
ROUTER_API_KEY: apiKey,
|
||||
NODE_ENV: "production"
|
||||
},
|
||||
detached: false,
|
||||
stdio: ["ignore", "pipe", "pipe"]
|
||||
});
|
||||
|
||||
serverPid = serverProcess.pid;
|
||||
|
||||
// Save PID to file
|
||||
fs.writeFileSync(PID_FILE, String(serverPid));
|
||||
|
||||
// Log server output
|
||||
serverProcess.stdout.on("data", (data) => {
|
||||
console.log(`[MITM Server] ${data.toString().trim()}`);
|
||||
});
|
||||
|
||||
serverProcess.stderr.on("data", (data) => {
|
||||
console.error(`[MITM Server Error] ${data.toString().trim()}`);
|
||||
});
|
||||
|
||||
serverProcess.on("exit", (code) => {
|
||||
console.log(`MITM server exited with code ${code}`);
|
||||
serverProcess = null;
|
||||
serverPid = null;
|
||||
|
||||
// Remove PID file
|
||||
try {
|
||||
fs.unlinkSync(PID_FILE);
|
||||
} catch (error) {
|
||||
// Ignore
|
||||
}
|
||||
});
|
||||
|
||||
// Wait and verify server actually started
|
||||
const started = await new Promise((resolve) => {
|
||||
let resolved = false;
|
||||
const timeout = setTimeout(() => {
|
||||
if (!resolved) { resolved = true; resolve(true); }
|
||||
}, 2000);
|
||||
|
||||
serverProcess.on("exit", (code) => {
|
||||
clearTimeout(timeout);
|
||||
if (!resolved) { resolved = true; resolve(false); }
|
||||
});
|
||||
|
||||
// Check stderr for error messages
|
||||
serverProcess.stderr.on("data", (data) => {
|
||||
const msg = data.toString().trim();
|
||||
if (msg.includes("Port") && msg.includes("already in use")) {
|
||||
clearTimeout(timeout);
|
||||
if (!resolved) { resolved = true; resolve(false); }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (!started) {
|
||||
throw new Error("MITM server failed to start (port 443 may be in use)");
|
||||
}
|
||||
|
||||
return {
|
||||
running: true,
|
||||
pid: serverPid
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop MITM proxy
|
||||
* @param {string} sudoPassword - Sudo password for DNS cleanup
|
||||
*/
|
||||
async function stopMitm(sudoPassword) {
|
||||
// 1. Kill server process (in-memory or from PID file)
|
||||
const proc = serverProcess;
|
||||
if (proc && !proc.killed) {
|
||||
console.log("Stopping MITM server...");
|
||||
proc.kill("SIGTERM");
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
if (!proc.killed) {
|
||||
proc.kill("SIGKILL");
|
||||
}
|
||||
serverProcess = null;
|
||||
serverPid = null;
|
||||
} else {
|
||||
// Fallback: kill by PID file
|
||||
try {
|
||||
if (fs.existsSync(PID_FILE)) {
|
||||
const savedPid = parseInt(fs.readFileSync(PID_FILE, "utf-8").trim(), 10);
|
||||
if (savedPid && isProcessAlive(savedPid)) {
|
||||
console.log(`Killing MITM server (PID: ${savedPid})...`);
|
||||
process.kill(savedPid, "SIGTERM");
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
if (isProcessAlive(savedPid)) {
|
||||
process.kill(savedPid, "SIGKILL");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
serverProcess = null;
|
||||
serverPid = null;
|
||||
}
|
||||
|
||||
// 2. Remove DNS entry
|
||||
console.log("Removing DNS entry...");
|
||||
await removeDNSEntry(sudoPassword);
|
||||
|
||||
// 3. Remove PID file
|
||||
try {
|
||||
fs.unlinkSync(PID_FILE);
|
||||
} catch (error) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
return {
|
||||
running: false,
|
||||
pid: null
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getMitmStatus,
|
||||
startMitm,
|
||||
stopMitm,
|
||||
getCachedPassword,
|
||||
setCachedPassword
|
||||
};
|
||||
@@ -0,0 +1,214 @@
|
||||
const https = require("https");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const dns = require("dns");
|
||||
const { promisify } = require("util");
|
||||
const os = require("os");
|
||||
|
||||
// Configuration
|
||||
const TARGET_HOST = "daily-cloudcode-pa.googleapis.com";
|
||||
const LOCAL_PORT = 443;
|
||||
const ROUTER_URL = "http://localhost:20128/v1/chat/completions";
|
||||
const API_KEY = process.env.ROUTER_API_KEY;
|
||||
const DB_FILE = path.join(os.homedir(), ".9router", "db.json");
|
||||
|
||||
// Toggle logging (set true to enable file logging for debugging)
|
||||
const ENABLE_FILE_LOG = false;
|
||||
|
||||
if (!API_KEY) {
|
||||
console.error("❌ ROUTER_API_KEY required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Load SSL certificates
|
||||
const certDir = path.join(os.homedir(), ".9router", "mitm");
|
||||
const sslOptions = {
|
||||
key: fs.readFileSync(path.join(certDir, "server.key")),
|
||||
cert: fs.readFileSync(path.join(certDir, "server.crt"))
|
||||
};
|
||||
|
||||
// Chat endpoints that should be intercepted
|
||||
const CHAT_URL_PATTERNS = [":generateContent", ":streamGenerateContent"];
|
||||
|
||||
// Log directory for request/response dumps
|
||||
const LOG_DIR = path.join(__dirname, "../../logs/mitm");
|
||||
if (ENABLE_FILE_LOG && !fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
|
||||
|
||||
function saveRequestLog(url, bodyBuffer) {
|
||||
if (!ENABLE_FILE_LOG) return;
|
||||
try {
|
||||
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const urlSlug = url.replace(/[^a-zA-Z0-9]/g, "_").substring(0, 60);
|
||||
const filePath = path.join(LOG_DIR, `${ts}_${urlSlug}.json`);
|
||||
const body = JSON.parse(bodyBuffer.toString());
|
||||
fs.writeFileSync(filePath, JSON.stringify(body, null, 2));
|
||||
console.log(`💾 Saved request: ${filePath}`);
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
function saveResponseLog(url, data) {
|
||||
if (!ENABLE_FILE_LOG) return;
|
||||
try {
|
||||
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const urlSlug = url.replace(/[^a-zA-Z0-9]/g, "_").substring(0, 60);
|
||||
const filePath = path.join(LOG_DIR, `${ts}_${urlSlug}_response.txt`);
|
||||
fs.writeFileSync(filePath, data);
|
||||
console.log(`💾 Saved response: ${filePath}`);
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve real IP of target host (bypass /etc/hosts)
|
||||
let cachedTargetIP = null;
|
||||
async function resolveTargetIP() {
|
||||
if (cachedTargetIP) return cachedTargetIP;
|
||||
const resolver = new dns.Resolver();
|
||||
resolver.setServers(["8.8.8.8"]);
|
||||
const resolve4 = promisify(resolver.resolve4.bind(resolver));
|
||||
const addresses = await resolve4(TARGET_HOST);
|
||||
cachedTargetIP = addresses[0];
|
||||
return cachedTargetIP;
|
||||
}
|
||||
|
||||
function collectBodyRaw(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
req.on("data", chunk => chunks.push(chunk));
|
||||
req.on("end", () => resolve(Buffer.concat(chunks)));
|
||||
req.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function extractModel(body) {
|
||||
try {
|
||||
return JSON.parse(body.toString()).model || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getMappedModel(model) {
|
||||
if (!model) return null;
|
||||
try {
|
||||
const db = JSON.parse(fs.readFileSync(DB_FILE, "utf-8"));
|
||||
return db.mitmAlias?.antigravity?.[model] || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function passthrough(req, res, bodyBuffer) {
|
||||
const targetIP = await resolveTargetIP();
|
||||
|
||||
const forwardReq = https.request({
|
||||
hostname: targetIP,
|
||||
port: 443,
|
||||
path: req.url,
|
||||
method: req.method,
|
||||
headers: { ...req.headers, host: TARGET_HOST },
|
||||
servername: TARGET_HOST,
|
||||
rejectUnauthorized: false
|
||||
}, (forwardRes) => {
|
||||
res.writeHead(forwardRes.statusCode, forwardRes.headers);
|
||||
forwardRes.pipe(res);
|
||||
});
|
||||
|
||||
forwardReq.on("error", (err) => {
|
||||
console.error(`❌ Passthrough error: ${err.message}`);
|
||||
if (!res.headersSent) res.writeHead(502);
|
||||
res.end("Bad Gateway");
|
||||
});
|
||||
|
||||
if (bodyBuffer.length > 0) forwardReq.write(bodyBuffer);
|
||||
forwardReq.end();
|
||||
}
|
||||
|
||||
async function intercept(req, res, bodyBuffer, mappedModel) {
|
||||
try {
|
||||
const body = JSON.parse(bodyBuffer.toString());
|
||||
body.model = mappedModel;
|
||||
|
||||
const response = await fetch(ROUTER_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${API_KEY}`
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errText = await response.text().catch(() => "");
|
||||
throw new Error(`9Router ${response.status}: ${errText}`);
|
||||
}
|
||||
|
||||
res.writeHead(200, {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
"Connection": "keep-alive",
|
||||
"X-Accel-Buffering": "no"
|
||||
});
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) { res.end(); break; }
|
||||
res.write(decoder.decode(value, { stream: true }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`❌ ${error.message}`);
|
||||
if (!res.headersSent) res.writeHead(500, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ error: { message: error.message, type: "mitm_error" } }));
|
||||
}
|
||||
}
|
||||
|
||||
const server = https.createServer(sslOptions, async (req, res) => {
|
||||
const bodyBuffer = await collectBodyRaw(req);
|
||||
|
||||
// Save request log if enabled
|
||||
if (bodyBuffer.length > 0) saveRequestLog(req.url, bodyBuffer);
|
||||
|
||||
// Anti-loop: requests from 9Router bypass interception
|
||||
if (req.headers["x-9router-source"] === "9router") {
|
||||
return passthrough(req, res, bodyBuffer);
|
||||
}
|
||||
|
||||
const isChatRequest = CHAT_URL_PATTERNS.some(p => req.url.includes(p));
|
||||
|
||||
if (!isChatRequest) {
|
||||
return passthrough(req, res, bodyBuffer);
|
||||
}
|
||||
|
||||
const model = extractModel(bodyBuffer);
|
||||
const mappedModel = getMappedModel(model);
|
||||
|
||||
if (!mappedModel) {
|
||||
return passthrough(req, res, bodyBuffer);
|
||||
}
|
||||
|
||||
console.log(`🔀 ${model} → ${mappedModel}`);
|
||||
return intercept(req, res, bodyBuffer, mappedModel);
|
||||
});
|
||||
|
||||
server.listen(LOCAL_PORT, () => {
|
||||
console.log(`🚀 MITM ready on :${LOCAL_PORT} → ${ROUTER_URL}`);
|
||||
});
|
||||
|
||||
server.on("error", (error) => {
|
||||
if (error.code === "EADDRINUSE") {
|
||||
console.error(`❌ Port ${LOCAL_PORT} already in use`);
|
||||
} else if (error.code === "EACCES") {
|
||||
console.error(`❌ Permission denied for port ${LOCAL_PORT}`);
|
||||
} else {
|
||||
console.error(`❌ ${error.message}`);
|
||||
}
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => { server.close(() => process.exit(0)); });
|
||||
process.on("SIGINT", () => { server.close(() => process.exit(0)); });
|
||||
Reference in New Issue
Block a user