Fix : MITM

This commit is contained in:
decolua
2026-03-05 21:13:09 +07:00
parent 1c3ba6ef69
commit f4e08fcd16
11 changed files with 497 additions and 150 deletions

203
README.md
View File

@@ -136,6 +136,209 @@ Default URLs:
---
## 🛠️ Supported CLI Tools
9Router works seamlessly with all major AI coding tools:
<div align="center">
<table>
<tr>
<td align="center" width="120">
<img src="./public/providers/claude.png" width="60" alt="Claude Code"/><br/>
<b>Claude Code</b>
</td>
<td align="center" width="120">
<img src="./public/providers/openclaw.png" width="60" alt="OpenClaw"/><br/>
<b>OpenClaw</b>
</td>
<td align="center" width="120">
<img src="./public/providers/codex.png" width="60" alt="Codex"/><br/>
<b>Codex</b>
</td>
<td align="center" width="120">
<img src="./public/providers/opencode.png" width="60" alt="OpenCode"/><br/>
<b>OpenCode</b>
</td>
<td align="center" width="120">
<img src="./public/providers/cursor.png" width="60" alt="Cursor"/><br/>
<b>Cursor</b>
</td>
</tr>
<tr>
<td align="center" width="120">
<img src="./public/providers/cline.png" width="60" alt="Cline"/><br/>
<b>Cline</b>
</td>
<td align="center" width="120">
<img src="./public/providers/continue.png" width="60" alt="Continue"/><br/>
<b>Continue</b>
</td>
<td align="center" width="120">
<img src="./public/providers/droid.png" width="60" alt="Droid"/><br/>
<b>Droid</b>
</td>
<td align="center" width="120">
<img src="./public/providers/roo.png" width="60" alt="Roo"/><br/>
<b>Roo</b>
</td>
<td align="center" width="120">
<img src="./public/providers/antigravity.png" width="60" alt="Antigravity"/><br/>
<b>Antigravity</b>
</td>
</tr>
</table>
</div>
---
## 🌐 Supported Providers
### 🆓 Free Providers (Unlimited)
<div align="center">
<table>
<tr>
<td align="center" width="150">
<img src="./public/providers/iflow.png" width="70" alt="iFlow"/><br/>
<b>iFlow AI</b><br/>
<sub>8+ models • Unlimited</sub>
</td>
<td align="center" width="150">
<img src="./public/providers/qwen.png" width="70" alt="Qwen"/><br/>
<b>Qwen Code</b><br/>
<sub>3+ models • Unlimited</sub>
</td>
<td align="center" width="150">
<img src="./public/providers/gemini-cli.png" width="70" alt="Gemini CLI"/><br/>
<b>Gemini CLI</b><br/>
<sub>180K/month FREE</sub>
</td>
<td align="center" width="150">
<img src="./public/providers/kiro.png" width="70" alt="Kiro"/><br/>
<b>Kiro AI</b><br/>
<sub>Claude • Unlimited</sub>
</td>
</tr>
</table>
</div>
### 🔐 OAuth Providers
<div align="center">
<table>
<tr>
<td align="center" width="120">
<img src="./public/providers/claude.png" width="60" alt="Claude Code"/><br/>
<b>Claude Code</b>
</td>
<td align="center" width="120">
<img src="./public/providers/antigravity.png" width="60" alt="Antigravity"/><br/>
<b>Antigravity</b>
</td>
<td align="center" width="120">
<img src="./public/providers/codex.png" width="60" alt="Codex"/><br/>
<b>Codex</b>
</td>
<td align="center" width="120">
<img src="./public/providers/github.png" width="60" alt="GitHub"/><br/>
<b>GitHub</b>
</td>
<td align="center" width="120">
<img src="./public/providers/cursor.png" width="60" alt="Cursor"/><br/>
<b>Cursor</b>
</td>
</tr>
</table>
</div>
### 🔑 API Key Providers (40+)
<div align="center">
<table>
<tr>
<td align="center" width="100">
<img src="./public/providers/openrouter.png" width="50" alt="OpenRouter"/><br/>
<sub>OpenRouter</sub>
</td>
<td align="center" width="100">
<img src="./public/providers/glm.png" width="50" alt="GLM"/><br/>
<sub>GLM</sub>
</td>
<td align="center" width="100">
<img src="./public/providers/kimi.png" width="50" alt="Kimi"/><br/>
<sub>Kimi</sub>
</td>
<td align="center" width="100">
<img src="./public/providers/minimax.png" width="50" alt="MiniMax"/><br/>
<sub>MiniMax</sub>
</td>
<td align="center" width="100">
<img src="./public/providers/openai.png" width="50" alt="OpenAI"/><br/>
<sub>OpenAI</sub>
</td>
<td align="center" width="100">
<img src="./public/providers/anthropic.png" width="50" alt="Anthropic"/><br/>
<sub>Anthropic</sub>
</td>
</tr>
<tr>
<td align="center" width="100">
<img src="./public/providers/gemini.png" width="50" alt="Gemini"/><br/>
<sub>Gemini</sub>
</td>
<td align="center" width="100">
<img src="./public/providers/deepseek.png" width="50" alt="DeepSeek"/><br/>
<sub>DeepSeek</sub>
</td>
<td align="center" width="100">
<img src="./public/providers/groq.png" width="50" alt="Groq"/><br/>
<sub>Groq</sub>
</td>
<td align="center" width="100">
<img src="./public/providers/xai.png" width="50" alt="xAI"/><br/>
<sub>xAI</sub>
</td>
<td align="center" width="100">
<img src="./public/providers/mistral.png" width="50" alt="Mistral"/><br/>
<sub>Mistral</sub>
</td>
<td align="center" width="100">
<img src="./public/providers/perplexity.png" width="50" alt="Perplexity"/><br/>
<sub>Perplexity</sub>
</td>
</tr>
<tr>
<td align="center" width="100">
<img src="./public/providers/together.png" width="50" alt="Together"/><br/>
<sub>Together AI</sub>
</td>
<td align="center" width="100">
<img src="./public/providers/fireworks.png" width="50" alt="Fireworks"/><br/>
<sub>Fireworks</sub>
</td>
<td align="center" width="100">
<img src="./public/providers/cerebras.png" width="50" alt="Cerebras"/><br/>
<sub>Cerebras</sub>
</td>
<td align="center" width="100">
<img src="./public/providers/cohere.png" width="50" alt="Cohere"/><br/>
<sub>Cohere</sub>
</td>
<td align="center" width="100">
<img src="./public/providers/nvidia.png" width="50" alt="NVIDIA"/><br/>
<sub>NVIDIA</sub>
</td>
<td align="center" width="100">
<img src="./public/providers/siliconflow.png" width="50" alt="SiliconFlow"/><br/>
<sub>SiliconFlow</sub>
</td>
</tr>
</table>
<p><i>...and 20+ more providers including Nebius, Chutes, Hyperbolic, and custom OpenAI/Anthropic compatible endpoints</i></p>
</div>
---
## 💡 Key Features
| Feature | What It Does | Why It Matters |

View File

@@ -1,6 +1,6 @@
{
"name": "9router-app",
"version": "0.3.29",
"version": "0.3.31",
"description": "9Router web dashboard",
"private": true,
"scripts": {

View File

@@ -10,6 +10,7 @@ import Image from "next/image";
* - Start/Stop DNS replaces Save Mappings button
* - Toggle switch removed; status badge is display-only
* - Skips sudo modal if password is already cached
* - Model mappings can only be edited when DNS is active
*/
export default function MitmToolCard({
tool,
@@ -17,7 +18,6 @@ export default function MitmToolCard({
onToggle,
serverRunning,
dnsActive,
certCovered,
hasCachedPassword,
apiKeys,
activeProviders,
@@ -104,10 +104,19 @@ export default function MitmToolCard({
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Failed to toggle DNS");
setMessage({
type: "success",
text: action === "enable" ? "DNS enabled — traffic intercepted" : "DNS disabled — traffic restored",
});
if (action === "enable") {
setMessage({
type: "success",
text: `DNS enabled successfully. Please restart ${tool.name} to apply changes.`,
});
} else {
setMessage({
type: "success",
text: "DNS disabled — traffic restored",
});
}
setShowPasswordModal(false);
setSudoPassword("");
onDnsChange?.(data);
@@ -154,7 +163,7 @@ export default function MitmToolCard({
<Badge variant="warning" size="sm">DNS off</Badge>
)}
</div>
<p className="text-xs text-text-muted truncate">{tool.mitmDomain}</p>
<p className="text-xs text-text-muted">Intercept {tool.name} requests via MITM proxy</p>
</div>
</div>
<span className={`material-symbols-outlined text-text-muted text-[20px] transition-transform ${isExpanded ? "rotate-180" : ""}`}>
@@ -166,19 +175,12 @@ export default function MitmToolCard({
<div className="mt-4 pt-4 border-t border-border flex flex-col gap-4">
{/* Info */}
<div className="flex flex-col gap-0.5 text-[11px] text-text-muted px-1">
<p>
<span className="font-medium text-text-main">Domain:</span>{" "}
<code className="text-[10px] bg-surface px-1 rounded">{tool.mitmDomain}</code>
{certCovered !== undefined && (
<span className={`ml-1.5 ${certCovered ? "text-green-600" : "text-red-500"}`}>
<span className="material-symbols-outlined text-[11px] align-middle">
{certCovered ? "verified" : "warning"}
</span>
{certCovered ? " cert OK" : " cert missing domain"}
</span>
)}
</p>
<p>Toggle DNS to redirect {tool.name} traffic through 9Router via MITM.</p>
{!dnsActive && (
<p className="text-amber-600 text-[10px] mt-1">
Enable DNS to edit model mappings
</p>
)}
</div>
{message && (
@@ -201,12 +203,13 @@ export default function MitmToolCard({
onChange={(e) => handleModelMappingChange(model.alias, e.target.value)}
onBlur={(e) => handleMappingBlur(model.alias, e.target.value)}
placeholder="provider/model-id"
className="flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50"
disabled={!dnsActive}
className={`flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 ${!dnsActive ? "opacity-50 cursor-not-allowed" : ""}`}
/>
<button
onClick={() => openModelSelector(model.alias)}
disabled={!hasActiveProviders}
className={`px-2 py-1.5 rounded border text-xs transition-colors shrink-0 ${hasActiveProviders ? "bg-surface border-border hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}
disabled={!hasActiveProviders || !dnsActive}
className={`px-2 py-1.5 rounded border text-xs transition-colors shrink-0 ${hasActiveProviders && dnsActive ? "bg-surface border-border hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border"}`}
>
Select
</button>

View File

@@ -12,7 +12,7 @@ export default function MitmPageClient() {
const [apiKeys, setApiKeys] = useState([]);
const [cloudEnabled, setCloudEnabled] = useState(false);
const [expandedTool, setExpandedTool] = useState(null);
const [mitmStatus, setMitmStatus] = useState({ running: false, certExists: false, dnsStatus: {}, certCoversTools: {}, hasCachedPassword: false });
const [mitmStatus, setMitmStatus] = useState({ running: false, certExists: false, dnsStatus: {}, hasCachedPassword: false });
useEffect(() => {
fetchConnections();
@@ -78,7 +78,6 @@ export default function MitmPageClient() {
onToggle={() => setExpandedTool(expandedTool === toolId ? null : toolId)}
serverRunning={mitmStatus.running}
dnsActive={mitmStatus.dnsStatus?.[toolId] || false}
certCovered={mitmStatus.certCoversTools?.[toolId] || false}
hasCachedPassword={mitmStatus.hasCachedPassword || false}
apiKeys={apiKeys}
activeProviders={getActiveProviders()}

View File

@@ -2,6 +2,7 @@
import { NextResponse } from "next/server";
import { getMitmAlias, setMitmAliasAll } from "@/models";
import { getMitmStatus } from "@/mitm/manager";
// GET - Get MITM aliases for a tool
export async function GET(request) {
@@ -25,6 +26,15 @@ export async function PUT(request) {
return NextResponse.json({ error: "tool and mappings required" }, { status: 400 });
}
// Check if DNS is enabled for this tool
const status = await getMitmStatus();
if (!status.dnsStatus || !status.dnsStatus[tool]) {
return NextResponse.json(
{ error: `DNS must be enabled for ${tool} before editing model mappings` },
{ status: 403 }
);
}
const filtered = {};
for (const [alias, model] of Object.entries(mappings)) {
if (model && model.trim()) {

View File

@@ -31,7 +31,6 @@ export async function GET() {
pid: status.pid || null,
certExists: status.certExists || false,
dnsStatus: status.dnsStatus || {},
certCoversTools: status.certCoversTools || {},
hasCachedPassword: !!getCachedPassword(),
});
} catch (error) {

View File

@@ -1,55 +1,32 @@
const path = require("path");
const fs = require("fs");
const { MITM_DIR } = require("../paths");
// Wildcard domains — covers all subdomains without needing cert update per tool
const WILDCARD_DOMAINS = [
"*.googleapis.com",
"*.githubcopilot.com",
"*.individual.githubcopilot.com",
"*.business.githubcopilot.com"
];
const { generateRootCA, loadRootCA, generateLeafCert } = require("./rootCA");
/**
* Generate self-signed SSL certificate with wildcard SAN.
* Covers all current and future MITM tool domains automatically.
* Uses selfsigned (pure JS, no openssl needed).
* Generate Root CA certificate (one-time setup)
* This replaces the old static wildcard cert approach
*/
async function generateCert() {
const certDir = MITM_DIR;
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: "9router-mitm" }];
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: WILDCARD_DOMAINS.map(domain => ({ type: 2, value: domain }))
}
]
});
fs.writeFileSync(keyPath, pems.private);
fs.writeFileSync(certPath, pems.cert);
console.log(`✅ Generated wildcard SSL certificate: ${WILDCARD_DOMAINS.join(", ")}`);
return { key: keyPath, cert: certPath };
return await generateRootCA();
}
module.exports = { generateCert };
/**
* Get certificate for a specific domain (dynamic generation)
* Used by SNICallback in server.js
*/
function getCertForDomain(domain) {
try {
const rootCA = loadRootCA();
const leafCert = generateLeafCert(domain, rootCA);
return {
key: leafCert.key,
cert: leafCert.cert
};
} catch (error) {
console.error(`Failed to generate cert for ${domain}:`, error.message);
return null;
}
}
module.exports = { generateCert, getCertForDomain };

View File

@@ -43,8 +43,8 @@ function checkCertInstalledMac(certPath) {
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) => {
// Check Root store for our Root CA by common name
exec("certutil -store Root \"9Router MITM Root CA\"", (error) => {
resolve(!error);
});
});
@@ -130,7 +130,7 @@ async function uninstallCertMac(sudoPassword, certPath) {
}
async function uninstallCertWindows() {
const psCommand = `Start-Process certutil -ArgumentList '-delstore','Root','daily-cloudcode-pa.googleapis.com' -Verb RunAs -Wait -WindowStyle Hidden`;
const psCommand = `Start-Process certutil -ArgumentList '-delstore','Root','9Router MITM Root CA' -Verb RunAs -Wait -WindowStyle Hidden`;
return new Promise((resolve, reject) => {
exec(
`powershell -NonInteractive -WindowStyle Hidden -Command "${psCommand}"`,
@@ -144,12 +144,12 @@ async function uninstallCertWindows() {
}
function checkCertInstalledLinux() {
const certFile = `${LINUX_CERT_DIR}/9router-mitm.crt`;
const certFile = `${LINUX_CERT_DIR}/9router-root-ca.crt`;
return Promise.resolve(fs.existsSync(certFile));
}
async function installCertLinux(sudoPassword, certPath) {
const destFile = `${LINUX_CERT_DIR}/9router-mitm.crt`;
const destFile = `${LINUX_CERT_DIR}/9router-root-ca.crt`;
// Try update-ca-certificates (Debian/Ubuntu), fallback to update-ca-trust (Fedora/RHEL)
const cmd = `cp "${certPath}" "${destFile}" && (update-ca-certificates 2>/dev/null || update-ca-trust 2>/dev/null || true)`;
try {
@@ -161,7 +161,7 @@ async function installCertLinux(sudoPassword, certPath) {
}
async function uninstallCertLinux(sudoPassword) {
const destFile = `${LINUX_CERT_DIR}/9router-mitm.crt`;
const destFile = `${LINUX_CERT_DIR}/9router-root-ca.crt`;
const cmd = `rm -f "${destFile}" && (update-ca-certificates 2>/dev/null || update-ca-trust 2>/dev/null || true)`;
try {
await execWithPassword(cmd, sudoPassword);

153
src/mitm/cert/rootCA.js Normal file
View File

@@ -0,0 +1,153 @@
const path = require("path");
const fs = require("fs");
const forge = require("node-forge");
const { MITM_DIR } = require("../paths");
const ROOT_CA_KEY_PATH = path.join(MITM_DIR, "rootCA.key");
const ROOT_CA_CERT_PATH = path.join(MITM_DIR, "rootCA.crt");
/**
* Generate Root CA certificate (only once)
* This Root CA will sign all dynamic leaf certificates
*/
async function generateRootCA() {
if (fs.existsSync(ROOT_CA_KEY_PATH) && fs.existsSync(ROOT_CA_CERT_PATH)) {
console.log("✅ Root CA already exists");
return { key: ROOT_CA_KEY_PATH, cert: ROOT_CA_CERT_PATH };
}
if (!fs.existsSync(MITM_DIR)) {
fs.mkdirSync(MITM_DIR, { recursive: true });
}
console.log("🔐 Generating Root CA certificate...");
// Generate RSA key pair
const keys = forge.pki.rsa.generateKeyPair(2048);
// Create Root CA certificate
const cert = forge.pki.createCertificate();
cert.publicKey = keys.publicKey;
cert.serialNumber = "01";
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10);
const attrs = [
{ name: "commonName", value: "9Router MITM Root CA" },
{ name: "organizationName", value: "9Router" },
{ name: "countryName", value: "US" }
];
cert.setSubject(attrs);
cert.setIssuer(attrs); // Self-signed
cert.setExtensions([
{
name: "basicConstraints",
cA: true,
critical: true
},
{
name: "keyUsage",
keyCertSign: true,
cRLSign: true,
critical: true
},
{
name: "subjectKeyIdentifier"
}
]);
// Self-sign the certificate
cert.sign(keys.privateKey, forge.md.sha256.create());
// Save to disk
const privateKeyPem = forge.pki.privateKeyToPem(keys.privateKey);
const certPem = forge.pki.certificateToPem(cert);
fs.writeFileSync(ROOT_CA_KEY_PATH, privateKeyPem);
fs.writeFileSync(ROOT_CA_CERT_PATH, certPem);
console.log("✅ Root CA generated successfully");
return { key: ROOT_CA_KEY_PATH, cert: ROOT_CA_CERT_PATH };
}
/**
* Load Root CA from disk
*/
function loadRootCA() {
if (!fs.existsSync(ROOT_CA_KEY_PATH) || !fs.existsSync(ROOT_CA_CERT_PATH)) {
throw new Error("Root CA not found. Generate it first.");
}
const keyPem = fs.readFileSync(ROOT_CA_KEY_PATH, "utf8");
const certPem = fs.readFileSync(ROOT_CA_CERT_PATH, "utf8");
return {
key: forge.pki.privateKeyFromPem(keyPem),
cert: forge.pki.certificateFromPem(certPem)
};
}
/**
* Generate leaf certificate for a specific domain, signed by Root CA
*/
function generateLeafCert(domain, rootCA) {
// Generate key pair for leaf cert
const keys = forge.pki.rsa.generateKeyPair(2048);
// Create leaf certificate
const cert = forge.pki.createCertificate();
cert.publicKey = keys.publicKey;
cert.serialNumber = Math.floor(Math.random() * 1000000).toString();
cert.validity.notBefore = new Date();
cert.validity.notAfter = new Date();
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
cert.setSubject([
{ name: "commonName", value: domain }
]);
cert.setIssuer(rootCA.cert.subject.attributes);
cert.setExtensions([
{
name: "basicConstraints",
cA: false
},
{
name: "keyUsage",
digitalSignature: true,
keyEncipherment: true
},
{
name: "extKeyUsage",
serverAuth: true,
clientAuth: true
},
{
name: "subjectAltName",
altNames: [
{ type: 2, value: domain }, // DNS
{ type: 2, value: `*.${domain}` } // Wildcard
]
}
]);
// Sign with Root CA
cert.sign(rootCA.key, forge.md.sha256.create());
return {
key: forge.pki.privateKeyToPem(keys.privateKey),
cert: forge.pki.certificateToPem(cert)
};
}
module.exports = {
generateRootCA,
loadRootCA,
generateLeafCert,
ROOT_CA_CERT_PATH,
ROOT_CA_KEY_PATH
};

View File

@@ -245,37 +245,6 @@ function pollMitmHealth(timeoutMs, port = MITM_PORT) {
});
}
/**
* Check which tools have their domains covered by the installed cert SAN.
* Uses built-in crypto.X509Certificate (Node 15.6+).
*/
function getCertToolCoverage(certPath) {
try {
const pem = fs.readFileSync(certPath, "utf8");
const cert = new crypto.X509Certificate(pem);
const san = cert.subjectAltName || "";
// Extract all DNS SANs
const sans = san.split(",").map(s => s.trim().replace(/^DNS:/, ""));
const matchesSan = (domain) => sans.some(s => {
if (s === domain) return true;
// Wildcard: *.foo.com matches bar.foo.com
if (s.startsWith("*.")) {
const suffix = s.slice(1); // .foo.com
return domain.endsWith(suffix) && !domain.slice(0, -suffix.length).includes(".");
}
return false;
});
const { TOOL_HOSTS } = require("./dns/dnsConfig");
const coverage = {};
for (const [tool, hosts] of Object.entries(TOOL_HOSTS)) {
coverage[tool] = hosts.every(matchesSan);
}
return coverage;
} catch {
return {};
}
}
/**
* Get full MITM status including per-tool DNS status
*/
@@ -298,11 +267,10 @@ async function getMitmStatus() {
}
const dnsStatus = checkAllDNSStatus();
const certPath = path.join(MITM_DIR, "server.crt");
const certExists = fs.existsSync(certPath);
const certCoversTools = certExists ? getCertToolCoverage(certPath) : {};
const rootCACertPath = path.join(MITM_DIR, "rootCA.crt");
const certExists = fs.existsSync(rootCACertPath);
return { running, pid, certExists, dnsStatus, certCoversTools };
return { running, pid, certExists, dnsStatus };
}
/**
@@ -352,39 +320,34 @@ async function startServer(apiKey, sudoPassword) {
}
}
// Step 1: Generate SSL certificate if not exists or missing domain coverage
const certPath = path.join(MITM_DIR, "server.crt");
const keyPath = path.join(MITM_DIR, "server.key");
let needsRegenerate = false;
// Step 1: Auto-migration - Generate Root CA if not exists
const rootCACertPath = path.join(MITM_DIR, "rootCA.crt");
const rootCAKeyPath = path.join(MITM_DIR, "rootCA.key");
if (!fs.existsSync(certPath)) {
console.log("[MITM] Generating SSL certificate...");
needsRegenerate = true;
} else {
// Check if cert covers all tool domains
const coverage = getCertToolCoverage(certPath);
const { TOOL_HOSTS } = require("./dns/dnsConfig");
const allCovered = Object.keys(TOOL_HOSTS).every(tool => coverage[tool] === true);
if (!allCovered) {
console.log("[MITM] Certificate missing domain coverage — regenerating...");
needsRegenerate = true;
try {
fs.unlinkSync(certPath);
if (fs.existsSync(keyPath)) fs.unlinkSync(keyPath);
} catch { /* ignore */ }
}
}
if (needsRegenerate) {
if (!fs.existsSync(rootCACertPath) || !fs.existsSync(rootCAKeyPath)) {
console.log("[MITM] Generating Root CA certificate (first time or migration)...");
await generateCert();
}
// Step 2: Install cert + spawn server
// Step 1.5: Auto-install Root CA if not trusted yet
const { checkCertInstalled } = require("./cert/install");
const rootCATrusted = await checkCertInstalled(rootCACertPath);
if (!rootCATrusted) {
console.log("[MITM] Installing Root CA to system trust store...");
// Use provided password or cached/stored password
const password = sudoPassword || getCachedPassword() || await loadEncryptedPassword();
if (!password && !IS_WIN) {
throw new Error("Sudo password required to install Root CA certificate");
}
await installCert(password, rootCACertPath);
console.log("✅ Root CA installed successfully");
}
// Step 2: Spawn server (Root CA already installed in Step 1.5)
if (IS_WIN) {
const hostsFile = path.join(process.env.SystemRoot || "C:\\Windows", "System32", "drivers", "etc", "hosts");
const flagFile = path.join(os.tmpdir(), `mitm_ready_${Date.now()}.flag`);
const psSQ = (s) => s.replace(/'/g, "''");
const certPs = psSQ(certPath);
const nodePs = psSQ(process.execPath);
const serverPs = psSQ(SERVER_PATH);
const flagPs = psSQ(flagFile);
@@ -393,7 +356,6 @@ async function startServer(apiKey, sudoPassword) {
`$conn = Get-NetTCPConnection -LocalPort 443 -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1`,
`if ($conn -and $conn.OwningProcess -gt 4) { Stop-Process -Id $conn.OwningProcess -Force -ErrorAction SilentlyContinue }`,
`Start-Sleep -Milliseconds 500`,
`& certutil -addstore Root '${certPs}' | Out-Null`,
`$nodeCmd = 'set ROUTER_API_KEY=${psSQ(apiKey)}&& set NODE_ENV=production&& "${nodePs}" "${serverPs}"'`,
`Start-Process cmd -ArgumentList '/c',$nodeCmd -WindowStyle Hidden`,
`Start-Sleep -Milliseconds 500`,
@@ -429,13 +391,7 @@ async function startServer(apiKey, sudoPassword) {
if (_updateSettings) await _updateSettings({ mitmCertInstalled: true }).catch(() => { });
} else {
const { checkCertInstalled } = require("./cert/install");
const certTrusted = await checkCertInstalled(certPath);
if (!certTrusted) {
await installCert(sudoPassword, certPath);
if (_updateSettings) await _updateSettings({ mitmCertInstalled: true }).catch(() => { });
}
// Non-Windows: Root CA already installed in Step 1.5, just spawn server
const inlineCmd = `ROUTER_API_KEY='${apiKey}' NODE_ENV='production' '${process.execPath}' '${SERVER_PATH}'`;
serverProcess = spawn(
"sudo", ["-S", "-E", "sh", "-c", inlineCmd],
@@ -583,7 +539,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.");
await addDNSEntry(tool, sudoPassword);
// Use cached password if not provided
const password = sudoPassword || getCachedPassword() || await loadEncryptedPassword();
await addDNSEntry(tool, password);
return { success: true };
}
@@ -591,7 +550,9 @@ async function enableToolDNS(tool, sudoPassword) {
* Disable DNS for a specific tool
*/
async function disableToolDNS(tool, sudoPassword) {
await removeDNSEntry(tool, sudoPassword);
// Use cached password if not provided
const password = sudoPassword || getCachedPassword() || await loadEncryptedPassword();
await removeDNSEntry(tool, password);
return { success: true };
}

View File

@@ -26,15 +26,57 @@ if (!API_KEY) {
process.exit(1);
}
const { getCertForDomain } = require("./cert/generate");
// Certificate cache for performance
const certCache = new Map();
// SNI callback for dynamic certificate generation
function sniCallback(servername, cb) {
try {
// Check cache first
if (certCache.has(servername)) {
const cached = certCache.get(servername);
return cb(null, cached);
}
// Generate new cert for this domain
const certData = getCertForDomain(servername);
if (!certData) {
return cb(new Error(`Failed to generate cert for ${servername}`));
}
// Create secure context
const ctx = require("tls").createSecureContext({
key: certData.key,
cert: certData.cert
});
// Cache it
certCache.set(servername, ctx);
console.log(`✅ Generated cert for: ${servername}`);
cb(null, ctx);
} catch (error) {
console.error(`❌ SNI error for ${servername}:`, error.message);
cb(error);
}
}
// Load Root CA for default context
const certDir = MITM_DIR;
const rootCAKeyPath = path.join(certDir, "rootCA.key");
const rootCACertPath = path.join(certDir, "rootCA.crt");
let sslOptions;
try {
sslOptions = {
key: fs.readFileSync(path.join(certDir, "server.key")),
cert: fs.readFileSync(path.join(certDir, "server.crt"))
key: fs.readFileSync(rootCAKeyPath),
cert: fs.readFileSync(rootCACertPath),
SNICallback: sniCallback
};
} catch (e) {
console.error(`SSL cert not found in ${certDir}: ${e.message}`);
console.error(`Root CA not found in ${certDir}: ${e.message}`);
process.exit(1);
}