Spaces:
Runtime error
Runtime error
| export const runtime = "nodejs"; | |
| import fs from "fs"; | |
| import path from "path"; | |
| import { z } from "zod"; | |
| import { NextResponse } from "next/server"; | |
| import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; | |
| import { resolveApiKey } from "@/shared/services/apiKeyResolver"; | |
| import { resolveMitmDataDir } from "@/mitm/dataDir"; | |
| import { KIRO_MITM_PROFILE } from "@/mitm/targets/kiro"; | |
| import { ANTIGRAVITY_MITM_PROFILE } from "@/mitm/targets/antigravity"; | |
| type MitmTargetRoute = { | |
| id: string; | |
| name: string; | |
| targetHost: string; | |
| targetPort: number; | |
| localPort: number; | |
| endpoints: string[]; | |
| enabled: boolean; | |
| }; | |
| type MitmStats = { | |
| startedAt: string | null; | |
| totalRequests: number; | |
| interceptedRequests: number; | |
| activeConnections: number; | |
| lastRequestAt: string | null; | |
| lastInterceptAt: string | null; | |
| }; | |
| type MitmConfig = { | |
| port: number; | |
| targets: MitmTargetRoute[]; | |
| }; | |
| const DEFAULT_PORT = 443; | |
| const MITM_PORT_ERROR = | |
| "Transparent MITM interception currently requires port 443 because DNS override does not redirect destination ports."; | |
| const updateMitmSchema = z.object({ | |
| enabled: z.boolean().optional(), | |
| apiKey: z.string().optional(), | |
| keyId: z.string().optional(), | |
| sudoPassword: z.string().optional(), | |
| port: z.coerce.number().int().min(1).max(65535).optional(), | |
| }); | |
| const regenerateSchema = z.object({ | |
| action: z.literal("regenerate-cert").optional(), | |
| }); | |
| function getMitmDir() { | |
| return path.join(resolveMitmDataDir(), "mitm"); | |
| } | |
| function getConfigPath() { | |
| return path.join(getMitmDir(), "settings.json"); | |
| } | |
| function getStatsPath() { | |
| return path.join(getMitmDir(), "stats.json"); | |
| } | |
| function getCertPath() { | |
| return path.join(getMitmDir(), "server.crt"); | |
| } | |
| function getKeyPath() { | |
| return path.join(getMitmDir(), "server.key"); | |
| } | |
| function defaultTargets(port = DEFAULT_PORT): MitmTargetRoute[] { | |
| const allHosts = [ | |
| ANTIGRAVITY_MITM_PROFILE.targetHost, | |
| ...(ANTIGRAVITY_MITM_PROFILE.additionalHosts || []), | |
| ]; | |
| return [ | |
| { | |
| id: ANTIGRAVITY_MITM_PROFILE.id, | |
| name: ANTIGRAVITY_MITM_PROFILE.name, | |
| targetHost: allHosts.join(", "), | |
| targetPort: ANTIGRAVITY_MITM_PROFILE.targetPort, | |
| localPort: port, | |
| endpoints: ANTIGRAVITY_MITM_PROFILE.apiEndpoints, | |
| enabled: true, | |
| }, | |
| { | |
| id: KIRO_MITM_PROFILE.id, | |
| name: KIRO_MITM_PROFILE.name, | |
| targetHost: KIRO_MITM_PROFILE.targetHost, | |
| targetPort: KIRO_MITM_PROFILE.targetPort, | |
| localPort: KIRO_MITM_PROFILE.localPort, | |
| endpoints: KIRO_MITM_PROFILE.apiEndpoints, | |
| enabled: false, | |
| }, | |
| ]; | |
| } | |
| function readConfig(): MitmConfig { | |
| try { | |
| JSON.parse(fs.readFileSync(getConfigPath(), "utf8")); | |
| return { | |
| port: DEFAULT_PORT, | |
| targets: defaultTargets(DEFAULT_PORT), | |
| }; | |
| } catch { | |
| return { | |
| port: DEFAULT_PORT, | |
| targets: defaultTargets(DEFAULT_PORT), | |
| }; | |
| } | |
| } | |
| function writeConfig() { | |
| const mitmDir = getMitmDir(); | |
| fs.mkdirSync(mitmDir, { recursive: true }); | |
| fs.writeFileSync(getConfigPath(), JSON.stringify({ port: DEFAULT_PORT }, null, 2)); | |
| } | |
| function readStats(): MitmStats { | |
| try { | |
| const raw = JSON.parse(fs.readFileSync(getStatsPath(), "utf8")); | |
| return { | |
| startedAt: typeof raw.startedAt === "string" ? raw.startedAt : null, | |
| totalRequests: Number(raw.totalRequests || 0), | |
| interceptedRequests: Number(raw.interceptedRequests || 0), | |
| activeConnections: Number(raw.activeConnections || 0), | |
| lastRequestAt: typeof raw.lastRequestAt === "string" ? raw.lastRequestAt : null, | |
| lastInterceptAt: typeof raw.lastInterceptAt === "string" ? raw.lastInterceptAt : null, | |
| }; | |
| } catch { | |
| return { | |
| startedAt: null, | |
| totalRequests: 0, | |
| interceptedRequests: 0, | |
| activeConnections: 0, | |
| lastRequestAt: null, | |
| lastInterceptAt: null, | |
| }; | |
| } | |
| } | |
| async function buildMitmResponse() { | |
| const { getMitmStatus, getCachedPassword } = await import("@/mitm/manager.runtime"); | |
| const status = await getMitmStatus(); | |
| const config = readConfig(); | |
| const stats = readStats(); | |
| return { | |
| running: status.running, | |
| pid: status.pid || null, | |
| dnsConfigured: status.dnsConfigured || false, | |
| certExists: status.certExists || fs.existsSync(getCertPath()), | |
| hasCachedPassword: !!getCachedPassword(), | |
| port: config.port, | |
| targets: config.targets, | |
| stats, | |
| }; | |
| } | |
| export async function GET(request: Request) { | |
| const authError = await requireManagementAuth(request); | |
| if (authError) return authError; | |
| try { | |
| const { searchParams } = new URL(request.url); | |
| if (searchParams.get("download") === "cert") { | |
| const certPath = getCertPath(); | |
| if (!fs.existsSync(certPath)) { | |
| return NextResponse.json({ error: "MITM certificate not found" }, { status: 404 }); | |
| } | |
| return new NextResponse(fs.readFileSync(certPath), { | |
| headers: { | |
| "Content-Type": "application/x-pem-file", | |
| "Content-Disposition": 'attachment; filename="omniroute-mitm-ca.crt"', | |
| }, | |
| }); | |
| } | |
| return NextResponse.json(await buildMitmResponse()); | |
| } catch (error) { | |
| const message = error instanceof Error ? error.message : "Failed to load MITM settings"; | |
| return NextResponse.json({ error: message }, { status: 500 }); | |
| } | |
| } | |
| export async function PUT(request: Request) { | |
| const authError = await requireManagementAuth(request); | |
| if (authError) return authError; | |
| try { | |
| const rawBody = await request.json().catch(() => ({})); | |
| const parsed = updateMitmSchema.safeParse(rawBody); | |
| if (!parsed.success) { | |
| return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 }); | |
| } | |
| const config = readConfig(); | |
| if (parsed.data.port !== undefined && parsed.data.port !== DEFAULT_PORT) { | |
| return NextResponse.json({ error: MITM_PORT_ERROR }, { status: 400 }); | |
| } | |
| if (parsed.data.port !== undefined) { | |
| config.port = DEFAULT_PORT; | |
| config.targets = defaultTargets(config.port); | |
| writeConfig(); | |
| } | |
| if (typeof parsed.data.enabled === "boolean") { | |
| const { getCachedPassword, setCachedPassword, startMitm, stopMitm } = | |
| await import("@/mitm/manager.runtime"); | |
| const { isRoot } = await import("@/mitm/systemCommands"); | |
| const isWin = process.platform === "win32"; | |
| const isRootUser = !isWin && isRoot(); | |
| const sudoPassword = parsed.data.sudoPassword || getCachedPassword() || ""; | |
| if (parsed.data.enabled) { | |
| const apiKey = await resolveApiKey(parsed.data.keyId || null, parsed.data.apiKey || null); | |
| if (!apiKey || (!isWin && !isRootUser && !sudoPassword)) { | |
| return NextResponse.json( | |
| { error: isWin ? "Missing apiKey" : "Missing apiKey or sudoPassword" }, | |
| { status: 400 } | |
| ); | |
| } | |
| await startMitm(apiKey, sudoPassword, { port: config.port }); | |
| if (!isWin) setCachedPassword(sudoPassword); | |
| } else { | |
| if (!isWin && !isRootUser && !sudoPassword) { | |
| return NextResponse.json({ error: "Missing sudoPassword" }, { status: 400 }); | |
| } | |
| await stopMitm(sudoPassword); | |
| if (!isWin && parsed.data.sudoPassword) setCachedPassword(parsed.data.sudoPassword); | |
| } | |
| } | |
| return NextResponse.json(await buildMitmResponse()); | |
| } catch (error) { | |
| const message = error instanceof Error ? error.message : "Failed to update MITM settings"; | |
| return NextResponse.json({ error: message }, { status: 500 }); | |
| } | |
| } | |
| export async function POST(request: Request) { | |
| const authError = await requireManagementAuth(request); | |
| if (authError) return authError; | |
| try { | |
| const rawBody = await request.json().catch(() => ({})); | |
| const parsed = regenerateSchema.safeParse(rawBody); | |
| if (!parsed.success) { | |
| return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 }); | |
| } | |
| const { getMitmStatus } = await import("@/mitm/manager.runtime"); | |
| const status = await getMitmStatus(); | |
| if (status.running) { | |
| return NextResponse.json( | |
| { error: "Stop the MITM proxy before regenerating certificates" }, | |
| { status: 409 } | |
| ); | |
| } | |
| for (const filePath of [getCertPath(), getKeyPath()]) { | |
| try { | |
| if (fs.existsSync(filePath)) fs.unlinkSync(filePath); | |
| } catch { | |
| /* ignore */ | |
| } | |
| } | |
| const { generateCert } = await import("@/mitm/cert/generate"); | |
| await generateCert(); | |
| return NextResponse.json(await buildMitmResponse()); | |
| } catch (error) { | |
| const message = | |
| error instanceof Error ? error.message : "Failed to regenerate MITM certificate"; | |
| return NextResponse.json({ error: message }, { status: 500 }); | |
| } | |
| } | |