diff --git a/README.md b/README.md index 7634834..0e2e8d9 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,209 @@ Default URLs: --- +## 🛠️ Supported CLI Tools + +9Router works seamlessly with all major AI coding tools: + +
+ + + + + + + + + + + + + + + +
+ Claude Code
+ Claude Code +
+ OpenClaw
+ OpenClaw +
+ Codex
+ Codex +
+ OpenCode
+ OpenCode +
+ Cursor
+ Cursor +
+ Cline
+ Cline +
+ Continue
+ Continue +
+ Droid
+ Droid +
+ Roo
+ Roo +
+ Antigravity
+ Antigravity +
+
+ +--- + +## 🌐 Supported Providers + +### 🆓 Free Providers (Unlimited) + +
+ + + + + + + +
+ iFlow
+ iFlow AI
+ 8+ models • Unlimited +
+ Qwen
+ Qwen Code
+ 3+ models • Unlimited +
+ Gemini CLI
+ Gemini CLI
+ 180K/month FREE +
+ Kiro
+ Kiro AI
+ Claude • Unlimited +
+
+ +### 🔐 OAuth Providers + +
+ + + + + + + + +
+ Claude Code
+ Claude Code +
+ Antigravity
+ Antigravity +
+ Codex
+ Codex +
+ GitHub
+ GitHub +
+ Cursor
+ Cursor +
+
+ +### 🔑 API Key Providers (40+) + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ OpenRouter
+ OpenRouter +
+ GLM
+ GLM +
+ Kimi
+ Kimi +
+ MiniMax
+ MiniMax +
+ OpenAI
+ OpenAI +
+ Anthropic
+ Anthropic +
+ Gemini
+ Gemini +
+ DeepSeek
+ DeepSeek +
+ Groq
+ Groq +
+ xAI
+ xAI +
+ Mistral
+ Mistral +
+ Perplexity
+ Perplexity +
+ Together
+ Together AI +
+ Fireworks
+ Fireworks +
+ Cerebras
+ Cerebras +
+ Cohere
+ Cohere +
+ NVIDIA
+ NVIDIA +
+ SiliconFlow
+ SiliconFlow +
+

...and 20+ more providers including Nebius, Chutes, Hyperbolic, and custom OpenAI/Anthropic compatible endpoints

+
+ +--- + ## 💡 Key Features | Feature | What It Does | Why It Matters | diff --git a/package.json b/package.json index 1956842..ae3508e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "9router-app", - "version": "0.3.29", + "version": "0.3.31", "description": "9Router web dashboard", "private": true, "scripts": { diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/MitmToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/MitmToolCard.js index af0d9b2..88a5dfe 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/MitmToolCard.js +++ b/src/app/(dashboard)/dashboard/cli-tools/components/MitmToolCard.js @@ -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({ DNS off )} -

{tool.mitmDomain}

+

Intercept {tool.name} requests via MITM proxy

@@ -166,19 +175,12 @@ export default function MitmToolCard({
{/* Info */}
-

- Domain:{" "} - {tool.mitmDomain} - {certCovered !== undefined && ( - - - {certCovered ? "verified" : "warning"} - - {certCovered ? " cert OK" : " cert missing domain"} - - )} -

Toggle DNS to redirect {tool.name} traffic through 9Router via MITM.

+ {!dnsActive && ( +

+ ⚠️ Enable DNS to edit model mappings +

+ )}
{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" : ""}`} /> diff --git a/src/app/(dashboard)/dashboard/mitm/MitmPageClient.js b/src/app/(dashboard)/dashboard/mitm/MitmPageClient.js index 8ff00d3..6d4bff5 100644 --- a/src/app/(dashboard)/dashboard/mitm/MitmPageClient.js +++ b/src/app/(dashboard)/dashboard/mitm/MitmPageClient.js @@ -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()} diff --git a/src/app/api/cli-tools/antigravity-mitm/alias/route.js b/src/app/api/cli-tools/antigravity-mitm/alias/route.js index 568701d..de78ba5 100644 --- a/src/app/api/cli-tools/antigravity-mitm/alias/route.js +++ b/src/app/api/cli-tools/antigravity-mitm/alias/route.js @@ -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()) { diff --git a/src/app/api/cli-tools/antigravity-mitm/route.js b/src/app/api/cli-tools/antigravity-mitm/route.js index a891030..c8059fc 100644 --- a/src/app/api/cli-tools/antigravity-mitm/route.js +++ b/src/app/api/cli-tools/antigravity-mitm/route.js @@ -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) { diff --git a/src/mitm/cert/generate.js b/src/mitm/cert/generate.js index 5a4ff39..4fcc2de 100644 --- a/src/mitm/cert/generate.js +++ b/src/mitm/cert/generate.js @@ -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 }; diff --git a/src/mitm/cert/install.js b/src/mitm/cert/install.js index 98c04f4..b9aa774 100644 --- a/src/mitm/cert/install.js +++ b/src/mitm/cert/install.js @@ -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); diff --git a/src/mitm/cert/rootCA.js b/src/mitm/cert/rootCA.js new file mode 100644 index 0000000..014840f --- /dev/null +++ b/src/mitm/cert/rootCA.js @@ -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 +}; diff --git a/src/mitm/manager.js b/src/mitm/manager.js index 605d650..168d51f 100644 --- a/src/mitm/manager.js +++ b/src/mitm/manager.js @@ -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 }; } diff --git a/src/mitm/server.js b/src/mitm/server.js index 84ff54c..735cbe2 100644 --- a/src/mitm/server.js +++ b/src/mitm/server.js @@ -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); }