mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 20:00:47 +00:00
- Updated CLI tool components to accept initial status as a prop, improving state management for tool statuses.
- Added functionality to fetch and set statuses for various CLI tools (Claude, Codex, Droid, OpenClaw, Antigravity) on component mount. - Enhanced error handling and logging in the OAuth provider test utilities and DNS management functions. - Improved the MITM server to handle multiple target hosts and provide clearer error messages regarding port usage.
This commit is contained in:
+36
-20
@@ -3,7 +3,10 @@ const fs = require("fs");
|
||||
const path = require("path");
|
||||
const os = require("os");
|
||||
|
||||
const TARGET_HOST = "daily-cloudcode-pa.googleapis.com";
|
||||
const TARGET_HOSTS = [
|
||||
"daily-cloudcode-pa.googleapis.com",
|
||||
"cloudcode-pa.googleapis.com"
|
||||
];
|
||||
const IS_WIN = process.platform === "win32";
|
||||
const IS_MAC = process.platform === "darwin";
|
||||
const HOSTS_FILE = IS_WIN
|
||||
@@ -51,12 +54,16 @@ function execElevatedWindows(command) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if DNS entry already exists
|
||||
* Check if DNS entry already exists for a specific host
|
||||
*/
|
||||
function checkDNSEntry() {
|
||||
function checkDNSEntry(host = null) {
|
||||
try {
|
||||
const hostsContent = fs.readFileSync(HOSTS_FILE, "utf8");
|
||||
return hostsContent.includes(TARGET_HOST);
|
||||
if (host) {
|
||||
return hostsContent.includes(host);
|
||||
}
|
||||
// Check if all target hosts exist
|
||||
return TARGET_HOSTS.every(h => hostsContent.includes(h));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
@@ -66,19 +73,24 @@ function checkDNSEntry() {
|
||||
* Add DNS entry to hosts file
|
||||
*/
|
||||
async function addDNSEntry(sudoPassword) {
|
||||
if (checkDNSEntry()) {
|
||||
console.log(`DNS entry for ${TARGET_HOST} already exists`);
|
||||
const entriesToAdd = TARGET_HOSTS.filter(host => !checkDNSEntry(host));
|
||||
|
||||
if (entriesToAdd.length === 0) {
|
||||
console.log(`DNS entries for all target hosts already exist`);
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = `127.0.0.1 ${TARGET_HOST}`;
|
||||
const entries = entriesToAdd.map(host => `127.0.0.1 ${host}`).join("\n");
|
||||
|
||||
try {
|
||||
if (IS_WIN) {
|
||||
// Windows: use elevated echo >> hosts
|
||||
await execElevatedWindows(`echo ${entry} >> "${HOSTS_FILE}"`);
|
||||
// Windows: add each entry separately
|
||||
for (const host of entriesToAdd) {
|
||||
const entry = `127.0.0.1 ${host}`;
|
||||
await execElevatedWindows(`echo ${entry} >> "${HOSTS_FILE}"`);
|
||||
}
|
||||
} else {
|
||||
await execWithPassword(`echo "${entry}" >> ${HOSTS_FILE}`, sudoPassword);
|
||||
await execWithPassword(`echo "${entries}" >> ${HOSTS_FILE}`, sudoPassword);
|
||||
}
|
||||
// Flush DNS cache
|
||||
if (IS_WIN) {
|
||||
@@ -89,7 +101,7 @@ async function addDNSEntry(sudoPassword) {
|
||||
// Linux: try systemd-resolved, fall back silently
|
||||
await execWithPassword("resolvectl flush-caches 2>/dev/null || true", sudoPassword);
|
||||
}
|
||||
console.log(`✅ Added DNS entry: ${entry}`);
|
||||
console.log(`✅ Added DNS entries: ${entriesToAdd.join(", ")}`);
|
||||
} catch (error) {
|
||||
const msg = error.message?.includes("incorrect password") ? "Wrong sudo password" : "Failed to add DNS entry";
|
||||
throw new Error(msg);
|
||||
@@ -100,8 +112,10 @@ async function addDNSEntry(sudoPassword) {
|
||||
* Remove DNS entry from hosts file
|
||||
*/
|
||||
async function removeDNSEntry(sudoPassword) {
|
||||
if (!checkDNSEntry()) {
|
||||
console.log(`DNS entry for ${TARGET_HOST} does not exist`);
|
||||
const entriesToRemove = TARGET_HOSTS.filter(host => checkDNSEntry(host));
|
||||
|
||||
if (entriesToRemove.length === 0) {
|
||||
console.log(`DNS entries for target hosts do not exist`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -109,7 +123,7 @@ async function removeDNSEntry(sudoPassword) {
|
||||
if (IS_WIN) {
|
||||
// Read in Node, filter, write to temp file, then elevated-copy over hosts
|
||||
const content = fs.readFileSync(HOSTS_FILE, "utf8");
|
||||
const filtered = content.split(/\r?\n/).filter(l => !l.includes(TARGET_HOST)).join("\r\n");
|
||||
const filtered = content.split(/\r?\n/).filter(l => !TARGET_HOSTS.some(host => l.includes(host))).join("\r\n");
|
||||
if (!filtered.trim() && content.trim()) {
|
||||
throw new Error("Filtered hosts content is empty, aborting to prevent data loss");
|
||||
}
|
||||
@@ -125,11 +139,13 @@ async function removeDNSEntry(sudoPassword) {
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// sed -i '' is macOS syntax; Linux uses sed -i without the empty string arg
|
||||
const sedCmd = IS_MAC
|
||||
? `sed -i '' '/${TARGET_HOST}/d' ${HOSTS_FILE}`
|
||||
: `sed -i '/${TARGET_HOST}/d' ${HOSTS_FILE}`;
|
||||
await execWithPassword(sedCmd, sudoPassword);
|
||||
// Remove all target hosts using sed
|
||||
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);
|
||||
}
|
||||
}
|
||||
// Flush DNS cache
|
||||
if (IS_WIN) {
|
||||
@@ -139,7 +155,7 @@ async function removeDNSEntry(sudoPassword) {
|
||||
} else {
|
||||
await execWithPassword("resolvectl flush-caches 2>/dev/null || true", sudoPassword);
|
||||
}
|
||||
console.log(`✅ Removed DNS entry for ${TARGET_HOST}`);
|
||||
console.log(`✅ Removed DNS entries for ${entriesToRemove.join(", ")}`);
|
||||
} catch (error) {
|
||||
const msg = error.message?.includes("incorrect password") ? "Wrong sudo password" : "Failed to remove DNS entry";
|
||||
throw new Error(msg);
|
||||
|
||||
+41
-3
@@ -1,5 +1,4 @@
|
||||
const cp = require("child_process");
|
||||
const { exec } = cp;
|
||||
const { exec, spawn, execSync } = require("child_process");
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
@@ -42,6 +41,43 @@ const SERVER_PATH = resolveServerPath();
|
||||
const ENCRYPT_ALGO = "aes-256-gcm";
|
||||
const ENCRYPT_SALT = "9router-mitm-pwd";
|
||||
|
||||
/**
|
||||
* Get process name using port 443
|
||||
* @returns {string|null} Process name or null if not found
|
||||
*/
|
||||
function getProcessUsingPort443() {
|
||||
try {
|
||||
if (IS_WIN) {
|
||||
// Windows: use netstat to find PID, then tasklist to get process name
|
||||
const netstatResult = execSync("netstat -ano | findstr :443", { encoding: "utf8" });
|
||||
const lines = netstatResult.trim().split("\n");
|
||||
if (lines.length > 0) {
|
||||
// Extract PID from last column (format: TCP 0.0.0.0:443 0.0.0.0:0 LISTENING 1234)
|
||||
const pidMatch = lines[0].match(/\s+(\d+)\s*$/);
|
||||
if (pidMatch) {
|
||||
const pid = pidMatch[1];
|
||||
const tasklistResult = execSync(`tasklist /FI "PID eq ${pid}" /FO CSV /NH`, { encoding: "utf8" });
|
||||
const processMatch = tasklistResult.match(/"([^"]+)"/);
|
||||
if (processMatch) {
|
||||
return processMatch[1].replace(".exe", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// macOS/Linux: use lsof
|
||||
const result = execSync("lsof -i :443", { encoding: "utf8" });
|
||||
const lines = result.trim().split("\n");
|
||||
if (lines.length > 1) {
|
||||
const processName = lines[1].split(/\s+/)[0];
|
||||
return processName;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Store server process in-memory
|
||||
let serverProcess = null;
|
||||
let serverPid = null;
|
||||
@@ -441,7 +477,9 @@ async function startMitm(apiKey, sudoPassword) {
|
||||
if (!health) {
|
||||
if (IS_WIN) serverProcess = null;
|
||||
try { await removeDNSEntry(sudoPassword); } catch { /* best effort */ }
|
||||
const reason = startError || "Check sudo password or port 443 access.";
|
||||
const processUsing443 = getProcessUsingPort443();
|
||||
const portInfo = processUsing443 ? ` Port 443 already in use by ${processUsing443}.` : "";
|
||||
const reason = startError || `Check sudo password or port 443 access.${portInfo}`;
|
||||
throw new Error(`MITM server failed to start. ${reason}`);
|
||||
}
|
||||
|
||||
|
||||
+15
-10
@@ -7,7 +7,10 @@ const os = require("os");
|
||||
|
||||
// Configuration
|
||||
const INTERNAL_REQUEST_HEADER = { name: "x-request-source", value: "local" };
|
||||
const TARGET_HOST = "daily-cloudcode-pa.googleapis.com";
|
||||
const TARGET_HOSTS = [
|
||||
"daily-cloudcode-pa.googleapis.com",
|
||||
"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;
|
||||
@@ -69,15 +72,15 @@ function saveResponseLog(url, data) {
|
||||
}
|
||||
|
||||
// Resolve real IP of target host (bypass /etc/hosts)
|
||||
let cachedTargetIP = null;
|
||||
async function resolveTargetIP() {
|
||||
if (cachedTargetIP) return cachedTargetIP;
|
||||
const cachedTargetIPs = {};
|
||||
async function resolveTargetIP(hostname) {
|
||||
if (cachedTargetIPs[hostname]) return cachedTargetIPs[hostname];
|
||||
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;
|
||||
const addresses = await resolve4(hostname);
|
||||
cachedTargetIPs[hostname] = addresses[0];
|
||||
return cachedTargetIPs[hostname];
|
||||
}
|
||||
|
||||
function collectBodyRaw(req) {
|
||||
@@ -108,15 +111,16 @@ function getMappedModel(model) {
|
||||
}
|
||||
|
||||
async function passthrough(req, res, bodyBuffer) {
|
||||
const targetIP = await resolveTargetIP();
|
||||
const targetHost = req.headers.host || TARGET_HOSTS[0];
|
||||
const targetIP = await resolveTargetIP(targetHost);
|
||||
|
||||
const forwardReq = https.request({
|
||||
hostname: targetIP,
|
||||
port: 443,
|
||||
path: req.url,
|
||||
method: req.method,
|
||||
headers: { ...req.headers, host: TARGET_HOST },
|
||||
servername: TARGET_HOST,
|
||||
headers: { ...req.headers, host: targetHost },
|
||||
servername: targetHost,
|
||||
rejectUnauthorized: false
|
||||
}, (forwardRes) => {
|
||||
res.writeHead(forwardRes.statusCode, forwardRes.headers);
|
||||
@@ -210,6 +214,7 @@ const server = https.createServer(sslOptions, async (req, res) => {
|
||||
|
||||
server.listen(LOCAL_PORT, () => {
|
||||
console.log(`🚀 MITM ready on :${LOCAL_PORT} → ${ROUTER_URL}`);
|
||||
console.log(`📡 Intercepting: ${TARGET_HOSTS.join(", ")}`);
|
||||
});
|
||||
|
||||
server.on("error", (error) => {
|
||||
|
||||
Reference in New Issue
Block a user