responses-proxy 0.1.4 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,7 +22,7 @@
22
22
  margin: 0;
23
23
  }
24
24
  </style>
25
- <script type="module" crossorigin src="/assets/index-DAy1ivku.js"></script>
25
+ <script type="module" crossorigin src="/assets/index-D4Ktr4qL.js"></script>
26
26
  <link rel="stylesheet" crossorigin href="/assets/index-DpqgYK3L.css">
27
27
  </head>
28
28
  <body>
@@ -0,0 +1,111 @@
1
+ /**
2
+ * MITM Certificate Management — generates root CA + per-domain leaf certs.
3
+ * Uses node-forge for certificate generation (no native deps).
4
+ */
5
+ import { execSync } from "node:child_process";
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
+ import path from "node:path";
8
+ import { randomBytes } from "node:crypto";
9
+ const MITM_DIR = path.join(process.env.HOME || "", ".responses-proxy", "mitm");
10
+ const ROOT_CA_KEY = path.join(MITM_DIR, "rootCA.key");
11
+ const ROOT_CA_CERT = path.join(MITM_DIR, "rootCA.crt");
12
+ export function getMitmDir() {
13
+ mkdirSync(MITM_DIR, { recursive: true });
14
+ return MITM_DIR;
15
+ }
16
+ export function rootCaExists() {
17
+ return existsSync(ROOT_CA_KEY) && existsSync(ROOT_CA_CERT);
18
+ }
19
+ export function isRootCaTrusted() {
20
+ if (process.platform !== "darwin")
21
+ return false;
22
+ try {
23
+ const result = execSync(`security verify-cert -c "${ROOT_CA_CERT}" 2>&1`, { encoding: "utf8", timeout: 5000 });
24
+ return result.includes("successful") || !result.includes("CSSMERR");
25
+ }
26
+ catch {
27
+ return false;
28
+ }
29
+ }
30
+ /**
31
+ * Generate a self-signed Root CA using openssl CLI (available on macOS/Linux).
32
+ * Outputs rootCA.key + rootCA.crt to MITM_DIR.
33
+ */
34
+ export function generateRootCA() {
35
+ mkdirSync(MITM_DIR, { recursive: true });
36
+ if (rootCaExists()) {
37
+ return { keyPath: ROOT_CA_KEY, certPath: ROOT_CA_CERT };
38
+ }
39
+ // Generate RSA 2048 key
40
+ execSync(`openssl genrsa -out "${ROOT_CA_KEY}" 2048`, { stdio: "ignore", timeout: 10000 });
41
+ // Generate self-signed CA cert (valid 10 years)
42
+ execSync(`openssl req -x509 -new -nodes -key "${ROOT_CA_KEY}" -sha256 -days 3650 ` +
43
+ `-subj "/CN=responses-proxy MITM CA/O=responses-proxy/C=US" ` +
44
+ `-out "${ROOT_CA_CERT}"`, { stdio: "ignore", timeout: 10000 });
45
+ return { keyPath: ROOT_CA_KEY, certPath: ROOT_CA_CERT };
46
+ }
47
+ /**
48
+ * Trust the root CA in the macOS system keychain (requires sudo).
49
+ */
50
+ export async function trustRootCA(sudoPassword) {
51
+ if (process.platform !== "darwin") {
52
+ throw new Error("Trust cert is only supported on macOS");
53
+ }
54
+ if (!rootCaExists()) {
55
+ throw new Error("Root CA does not exist. Generate it first.");
56
+ }
57
+ const cmd = `security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${ROOT_CA_CERT}"`;
58
+ if (sudoPassword) {
59
+ const { execWithPassword } = await import("./dns.js");
60
+ await execWithPassword(cmd, sudoPassword);
61
+ }
62
+ else {
63
+ execSync(`sudo ${cmd}`, { stdio: "ignore", timeout: 15000 });
64
+ }
65
+ }
66
+ /**
67
+ * Generate a leaf certificate for a specific domain, signed by the root CA.
68
+ * Returns { key, cert } as PEM strings.
69
+ */
70
+ export function generateLeafCert(domain) {
71
+ if (!rootCaExists())
72
+ return null;
73
+ const tmpDir = path.join(MITM_DIR, "tmp");
74
+ mkdirSync(tmpDir, { recursive: true });
75
+ const serial = randomBytes(8).toString("hex");
76
+ const leafKey = path.join(tmpDir, `${serial}.key`);
77
+ const leafCsr = path.join(tmpDir, `${serial}.csr`);
78
+ const leafCert = path.join(tmpDir, `${serial}.crt`);
79
+ const extFile = path.join(tmpDir, `${serial}.ext`);
80
+ try {
81
+ // Generate leaf key
82
+ execSync(`openssl genrsa -out "${leafKey}" 2048`, { stdio: "ignore", timeout: 5000 });
83
+ // Generate CSR
84
+ execSync(`openssl req -new -key "${leafKey}" -subj "/CN=${domain}" -out "${leafCsr}"`, { stdio: "ignore", timeout: 5000 });
85
+ // Write SAN extension file
86
+ writeFileSync(extFile, `authorityKeyIdentifier=keyid,issuer\nbasicConstraints=CA:FALSE\nkeyUsage=digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment\nsubjectAltName=DNS:${domain}\n`);
87
+ // Sign with root CA
88
+ execSync(`openssl x509 -req -in "${leafCsr}" -CA "${ROOT_CA_CERT}" -CAkey "${ROOT_CA_KEY}" ` +
89
+ `-CAcreateserial -out "${leafCert}" -days 825 -sha256 -extfile "${extFile}"`, { stdio: "ignore", timeout: 5000 });
90
+ const key = readFileSync(leafKey, "utf8");
91
+ const cert = readFileSync(leafCert, "utf8");
92
+ // Cleanup temp files
93
+ try {
94
+ for (const f of [leafKey, leafCsr, leafCert, extFile]) {
95
+ if (existsSync(f))
96
+ require("node:fs").unlinkSync(f);
97
+ }
98
+ }
99
+ catch { /* best effort */ }
100
+ return { key, cert };
101
+ }
102
+ catch (error) {
103
+ return null;
104
+ }
105
+ }
106
+ export function loadRootCA() {
107
+ return {
108
+ key: readFileSync(ROOT_CA_KEY, "utf8"),
109
+ cert: readFileSync(ROOT_CA_CERT, "utf8"),
110
+ };
111
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * MITM DNS Management — manages /etc/hosts entries to redirect tool domains to localhost.
3
+ */
4
+ import { execSync, spawn } from "node:child_process";
5
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
6
+ const IS_WIN = process.platform === "win32";
7
+ const IS_MAC = process.platform === "darwin";
8
+ const HOSTS_FILE = IS_WIN
9
+ ? `${process.env.SystemRoot || "C:\\Windows"}\\System32\\drivers\\etc\\hosts`
10
+ : "/etc/hosts";
11
+ // Tool domains to intercept
12
+ export const TOOL_HOSTS = {
13
+ antigravity: ["daily-cloudcode-pa.googleapis.com", "cloudcode-pa.googleapis.com"],
14
+ copilot: ["api.individual.githubcopilot.com"],
15
+ kiro: ["q.us-east-1.amazonaws.com"],
16
+ };
17
+ export function isSudoAvailable() {
18
+ if (IS_WIN)
19
+ return false;
20
+ try {
21
+ execSync("command -v sudo", { stdio: "ignore", timeout: 3000 });
22
+ return true;
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ }
28
+ export function canRunSudoWithoutPassword() {
29
+ if (IS_WIN || !isSudoAvailable())
30
+ return true;
31
+ try {
32
+ execSync("sudo -n true", { stdio: "ignore", timeout: 3000 });
33
+ return true;
34
+ }
35
+ catch {
36
+ return false;
37
+ }
38
+ }
39
+ export function isSudoPasswordRequired() {
40
+ return !IS_WIN && isSudoAvailable() && !canRunSudoWithoutPassword();
41
+ }
42
+ export function execWithPassword(command, password) {
43
+ return new Promise((resolve, reject) => {
44
+ const useSudo = isSudoAvailable();
45
+ const child = useSudo
46
+ ? spawn("sudo", ["-S", "sh", "-c", command], { stdio: ["pipe", "pipe", "pipe"] })
47
+ : spawn("sh", ["-c", command], { stdio: ["ignore", "pipe", "pipe"] });
48
+ let stdout = "";
49
+ let stderr = "";
50
+ child.stdout?.on("data", (d) => { stdout += d; });
51
+ child.stderr?.on("data", (d) => { stderr += d; });
52
+ child.on("close", (code) => {
53
+ if (code === 0)
54
+ resolve(stdout);
55
+ else
56
+ reject(new Error(stderr || `Exit code ${code}`));
57
+ });
58
+ if (useSudo && child.stdin) {
59
+ child.stdin.write(`${password}\n`);
60
+ child.stdin.end();
61
+ }
62
+ });
63
+ }
64
+ function checkDNSEntry(host) {
65
+ try {
66
+ const content = readFileSync(HOSTS_FILE, "utf8");
67
+ return content.includes(host);
68
+ }
69
+ catch {
70
+ return false;
71
+ }
72
+ }
73
+ export function checkAllDNSStatus() {
74
+ try {
75
+ const content = readFileSync(HOSTS_FILE, "utf8");
76
+ const result = {};
77
+ for (const [tool, hosts] of Object.entries(TOOL_HOSTS)) {
78
+ result[tool] = hosts.every((h) => content.includes(h));
79
+ }
80
+ return result;
81
+ }
82
+ catch {
83
+ return Object.fromEntries(Object.keys(TOOL_HOSTS).map((t) => [t, false]));
84
+ }
85
+ }
86
+ async function flushDNS(sudoPassword) {
87
+ if (IS_WIN)
88
+ return;
89
+ if (IS_MAC) {
90
+ await execWithPassword("dscacheutil -flushcache && killall -HUP mDNSResponder", sudoPassword);
91
+ }
92
+ else {
93
+ await execWithPassword("resolvectl flush-caches 2>/dev/null || true", sudoPassword);
94
+ }
95
+ }
96
+ export async function addDNSEntry(tool, sudoPassword) {
97
+ const hosts = TOOL_HOSTS[tool];
98
+ if (!hosts)
99
+ throw new Error(`Unknown tool: ${tool}`);
100
+ const entriesToAdd = hosts.filter((h) => !checkDNSEntry(h));
101
+ if (entriesToAdd.length === 0)
102
+ return;
103
+ const current = readFileSync(HOSTS_FILE, "utf8");
104
+ const trimmed = current.replace(/[\r\n\s]+$/g, "");
105
+ const toAppend = entriesToAdd.map((h) => `127.0.0.1 ${h}`).join("\n");
106
+ const next = `${trimmed}\n${toAppend}\n`;
107
+ const escaped = next.replace(/'/g, "'\\''");
108
+ await execWithPassword(`printf '%s' '${escaped}' | tee ${HOSTS_FILE} > /dev/null`, sudoPassword);
109
+ await flushDNS(sudoPassword);
110
+ }
111
+ export async function removeDNSEntry(tool, sudoPassword) {
112
+ const hosts = TOOL_HOSTS[tool];
113
+ if (!hosts)
114
+ throw new Error(`Unknown tool: ${tool}`);
115
+ const entriesToRemove = hosts.filter((h) => checkDNSEntry(h));
116
+ if (entriesToRemove.length === 0)
117
+ return;
118
+ const current = readFileSync(HOSTS_FILE, "utf8");
119
+ const filtered = current
120
+ .split(/\r?\n/)
121
+ .filter((l) => !entriesToRemove.some((h) => l.includes(h)))
122
+ .join("\n");
123
+ const next = filtered.replace(/[\r\n\s]+$/g, "") + "\n";
124
+ const escaped = next.replace(/'/g, "'\\''");
125
+ await execWithPassword(`printf '%s' '${escaped}' | tee ${HOSTS_FILE} > /dev/null`, sudoPassword);
126
+ await flushDNS(sudoPassword);
127
+ }
128
+ export function removeAllDNSEntriesSync() {
129
+ try {
130
+ if (!existsSync(HOSTS_FILE))
131
+ return;
132
+ const allHosts = Object.values(TOOL_HOSTS).flat();
133
+ const content = readFileSync(HOSTS_FILE, "utf8");
134
+ const filtered = content
135
+ .split(/\r?\n/)
136
+ .filter((l) => !allHosts.some((h) => l.includes(h)))
137
+ .join("\n");
138
+ const next = filtered.replace(/[\r\n\s]+$/g, "") + "\n";
139
+ if (next === content)
140
+ return;
141
+ writeFileSync(HOSTS_FILE, next, "utf8");
142
+ if (IS_MAC) {
143
+ try {
144
+ execSync("dscacheutil -flushcache && killall -HUP mDNSResponder", { stdio: "ignore" });
145
+ }
146
+ catch { /* ignore */ }
147
+ }
148
+ }
149
+ catch { /* best effort during shutdown */ }
150
+ }
@@ -0,0 +1,229 @@
1
+ /**
2
+ * MITM Proxy Server — intercepts HTTPS on port 443 with dynamic SSL certs.
3
+ *
4
+ * Cloned from 9Router's src/mitm/server.js approach:
5
+ * - HTTPS server on :443 with SNI callback for per-domain certs
6
+ * - Intercepts requests to tool domains (Antigravity, Copilot, Kiro)
7
+ * - Rewrites model in request body to mapped model from proxy
8
+ * - Forwards to real upstream via HTTPS with custom DNS (bypass /etc/hosts loop)
9
+ */
10
+ import * as https from "node:https";
11
+ import * as tls from "node:tls";
12
+ import * as dns from "node:dns";
13
+ import { promisify } from "node:util";
14
+ import { readFileSync, existsSync, writeFileSync } from "node:fs";
15
+ import path from "node:path";
16
+ import { execSync } from "node:child_process";
17
+ import { generateLeafCert, loadRootCA, rootCaExists, getMitmDir } from "./cert.js";
18
+ import { TOOL_HOSTS, removeAllDNSEntriesSync } from "./dns.js";
19
+ const LOCAL_PORT = 443;
20
+ const MITM_DIR = getMitmDir();
21
+ const PID_FILE = path.join(MITM_DIR, ".mitm.pid");
22
+ // Tool detection from host header
23
+ const HOST_TO_TOOL = {};
24
+ for (const [tool, hosts] of Object.entries(TOOL_HOSTS)) {
25
+ for (const h of hosts)
26
+ HOST_TO_TOOL[h] = tool;
27
+ }
28
+ // Proxy base URL — requests are forwarded to responses-proxy for model routing
29
+ const ROUTER_BASE_URL = process.env.MITM_ROUTER_BASE_URL || "http://localhost:8318";
30
+ // Internal header to prevent loop
31
+ const INTERNAL_HEADER = "x-request-source";
32
+ const INTERNAL_VALUE = "mitm-local";
33
+ // ─── SSL / SNI ───────────────────────────────────────────────────────────────
34
+ const certCache = new Map();
35
+ let rootCAPem;
36
+ function sniCallback(servername, cb) {
37
+ try {
38
+ if (certCache.has(servername))
39
+ return cb(null, certCache.get(servername));
40
+ const certData = generateLeafCert(servername);
41
+ if (!certData)
42
+ return cb(new Error(`Failed to generate cert for ${servername}`));
43
+ const ctx = tls.createSecureContext({
44
+ key: certData.key,
45
+ cert: `${certData.cert}\n${rootCAPem}`,
46
+ });
47
+ certCache.set(servername, ctx);
48
+ cb(null, ctx);
49
+ }
50
+ catch (e) {
51
+ cb(e);
52
+ }
53
+ }
54
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
55
+ const resolve4 = promisify((() => {
56
+ const resolver = new dns.Resolver();
57
+ resolver.setServers(["8.8.8.8", "1.1.1.1"]);
58
+ return resolver.resolve4.bind(resolver);
59
+ })());
60
+ const ipCache = {};
61
+ async function resolveTargetIP(hostname) {
62
+ const cached = ipCache[hostname];
63
+ if (cached && Date.now() - cached.ts < 5 * 60 * 1000)
64
+ return cached.ip;
65
+ const addresses = await resolve4(hostname);
66
+ ipCache[hostname] = { ip: addresses[0], ts: Date.now() };
67
+ return addresses[0];
68
+ }
69
+ function collectBody(req) {
70
+ return new Promise((resolve, reject) => {
71
+ const chunks = [];
72
+ req.on("data", (chunk) => chunks.push(chunk));
73
+ req.on("end", () => resolve(Buffer.concat(chunks)));
74
+ req.on("error", reject);
75
+ });
76
+ }
77
+ // ─── Request handler ─────────────────────────────────────────────────────────
78
+ async function handleRequest(req, res) {
79
+ try {
80
+ // Health check
81
+ if (req.url === "/_mitm_health") {
82
+ res.writeHead(200, { "Content-Type": "application/json" });
83
+ res.end(JSON.stringify({ ok: true, pid: process.pid }));
84
+ return;
85
+ }
86
+ const bodyBuffer = await collectBody(req);
87
+ // Anti-loop: skip our own requests
88
+ if (req.headers[INTERNAL_HEADER] === INTERNAL_VALUE) {
89
+ return passthrough(req, res, bodyBuffer);
90
+ }
91
+ const host = (req.headers.host || "").split(":")[0];
92
+ const tool = HOST_TO_TOOL[host];
93
+ // Not a tool domain — passthrough
94
+ if (!tool) {
95
+ return passthrough(req, res, bodyBuffer);
96
+ }
97
+ // Forward to our proxy router instead of upstream
98
+ // This lets the proxy handle model routing, RTK, combos, etc.
99
+ return forwardToRouter(req, res, bodyBuffer, tool);
100
+ }
101
+ catch (e) {
102
+ if (!res.headersSent)
103
+ res.writeHead(500, { "Content-Type": "application/json" });
104
+ res.end(JSON.stringify({ error: { message: e.message, type: "mitm_error" } }));
105
+ }
106
+ }
107
+ /**
108
+ * Forward intercepted request to the local responses-proxy router.
109
+ * The proxy will handle model selection, provider routing, RTK, etc.
110
+ */
111
+ function forwardToRouter(req, res, bodyBuffer, tool) {
112
+ const url = new URL(ROUTER_BASE_URL);
113
+ const http = require("node:http");
114
+ const proxyReq = http.request({
115
+ hostname: url.hostname,
116
+ port: url.port || 8318,
117
+ path: "/v1/chat/completions",
118
+ method: "POST",
119
+ headers: {
120
+ "content-type": "application/json",
121
+ "content-length": String(bodyBuffer.length),
122
+ [INTERNAL_HEADER]: INTERNAL_VALUE,
123
+ "x-mitm-tool": tool,
124
+ "x-mitm-original-host": req.headers.host || "",
125
+ "x-mitm-original-path": req.url || "",
126
+ },
127
+ }, (proxyRes) => {
128
+ res.writeHead(proxyRes.statusCode, proxyRes.headers);
129
+ proxyRes.pipe(res);
130
+ });
131
+ proxyReq.on("error", (e) => {
132
+ // If proxy is unreachable, passthrough to real upstream
133
+ passthrough(req, res, bodyBuffer);
134
+ });
135
+ proxyReq.write(bodyBuffer);
136
+ proxyReq.end();
137
+ }
138
+ /**
139
+ * Forward request to the real upstream (bypass /etc/hosts via custom DNS).
140
+ */
141
+ async function passthrough(req, res, bodyBuffer) {
142
+ const originalHost = (req.headers.host || "").split(":")[0];
143
+ const targetIP = await resolveTargetIP(originalHost);
144
+ const forwardReq = https.request({
145
+ hostname: targetIP,
146
+ port: 443,
147
+ path: req.url,
148
+ method: req.method,
149
+ headers: { ...req.headers, host: originalHost },
150
+ servername: originalHost,
151
+ rejectUnauthorized: false,
152
+ }, (forwardRes) => {
153
+ res.writeHead(forwardRes.statusCode || 502, forwardRes.headers);
154
+ forwardRes.pipe(res);
155
+ });
156
+ forwardReq.on("error", (e) => {
157
+ if (!res.headersSent)
158
+ res.writeHead(502);
159
+ res.end("Bad Gateway");
160
+ });
161
+ if (bodyBuffer.length > 0)
162
+ forwardReq.write(bodyBuffer);
163
+ forwardReq.end();
164
+ }
165
+ // ─── Server startup ──────────────────────────────────────────────────────────
166
+ export function startMitmServer() {
167
+ if (!rootCaExists()) {
168
+ throw new Error("Root CA not found. Generate it first via the dashboard.");
169
+ }
170
+ const rootCA = loadRootCA();
171
+ rootCAPem = rootCA.cert;
172
+ const sslOptions = {
173
+ key: rootCA.key,
174
+ cert: rootCA.cert,
175
+ SNICallback: sniCallback,
176
+ };
177
+ // Kill existing process on port 443
178
+ try {
179
+ const pids = execSync(`lsof -nP -iTCP:${LOCAL_PORT} -sTCP:LISTEN -t`, { encoding: "utf8", timeout: 3000 }).trim();
180
+ if (pids) {
181
+ pids.split("\n").forEach((pid) => {
182
+ try {
183
+ process.kill(Number(pid), "SIGKILL");
184
+ }
185
+ catch { /* ignore */ }
186
+ });
187
+ }
188
+ }
189
+ catch { /* port is free */ }
190
+ const server = https.createServer(sslOptions, handleRequest);
191
+ server.listen(LOCAL_PORT, () => {
192
+ console.log(`[mitm] 🚀 MITM server running on :${LOCAL_PORT}`);
193
+ writeFileSync(PID_FILE, String(process.pid));
194
+ });
195
+ server.on("error", (e) => {
196
+ if (e.code === "EADDRINUSE") {
197
+ throw new Error(`Port ${LOCAL_PORT} already in use`);
198
+ }
199
+ if (e.code === "EACCES") {
200
+ throw new Error(`Permission denied for port ${LOCAL_PORT}. Run with sudo.`);
201
+ }
202
+ throw e;
203
+ });
204
+ // Graceful shutdown
205
+ const shutdown = () => {
206
+ removeAllDNSEntriesSync();
207
+ server.close(() => process.exit(0));
208
+ setTimeout(() => process.exit(0), 1500);
209
+ };
210
+ process.on("SIGTERM", shutdown);
211
+ process.on("SIGINT", shutdown);
212
+ return { pid: process.pid };
213
+ }
214
+ export function getMitmPid() {
215
+ try {
216
+ if (!existsSync(PID_FILE))
217
+ return null;
218
+ const pid = Number(readFileSync(PID_FILE, "utf8").trim());
219
+ // Check if process is alive
220
+ process.kill(pid, 0);
221
+ return pid;
222
+ }
223
+ catch {
224
+ return null;
225
+ }
226
+ }
227
+ export function isMitmRunning() {
228
+ return getMitmPid() !== null;
229
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * MITM Server entry point — spawned with sudo by the main proxy.
3
+ * Runs the HTTPS interception server on port 443.
4
+ */
5
+ import { startMitmServer } from "./server.js";
6
+ try {
7
+ const { pid } = startMitmServer();
8
+ console.log(`[mitm] Server started (PID: ${pid})`);
9
+ }
10
+ catch (error) {
11
+ console.error(`[mitm] Failed to start:`, error instanceof Error ? error.message : error);
12
+ process.exit(1);
13
+ }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "responses-proxy-runtime",
3
- "version": "0.1.4",
3
+ "version": "0.2.1",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "dependencies": {
package/dist/server.js CHANGED
@@ -1972,33 +1972,122 @@ app.delete("/api/cli-tools/codex-settings", async (_request, reply) => {
1972
1972
  return reply.code(500).send({ error: error instanceof Error ? error.message : "Failed to reset settings" });
1973
1973
  }
1974
1974
  });
1975
- // ─── MITM Server Status (stub — requires host-mode deployment) ──────────────
1975
+ // ─── MITM Server Status ─────────────────────────────────────────────────────
1976
1976
  app.get("/api/cli-tools/mitm-status", async (_request, reply) => {
1977
- // MITM requires running directly on host (not Docker) with root privileges.
1978
- // Return stub status indicating MITM is not available in this deployment mode.
1979
- return reply.send({
1980
- running: false,
1981
- certExists: false,
1982
- certTrusted: false,
1983
- dnsStatus: {},
1984
- available: false,
1985
- reason: "MITM requires host-mode deployment with root privileges (port 443 + DNS + cert trust). Not available in Docker mode.",
1986
- });
1977
+ try {
1978
+ const { rootCaExists, isRootCaTrusted } = await import("./mitm/cert.js");
1979
+ const { isMitmRunning } = await import("./mitm/server.js");
1980
+ const { checkAllDNSStatus, isSudoPasswordRequired } = await import("./mitm/dns.js");
1981
+ return reply.send({
1982
+ running: isMitmRunning(),
1983
+ certExists: rootCaExists(),
1984
+ certTrusted: isRootCaTrusted(),
1985
+ dnsStatus: checkAllDNSStatus(),
1986
+ needsSudoPassword: isSudoPasswordRequired(),
1987
+ available: true,
1988
+ isWin: process.platform === "win32",
1989
+ });
1990
+ }
1991
+ catch (error) {
1992
+ return reply.send({
1993
+ running: false,
1994
+ certExists: false,
1995
+ certTrusted: false,
1996
+ dnsStatus: {},
1997
+ available: false,
1998
+ reason: error instanceof Error ? error.message : "MITM module not available",
1999
+ });
2000
+ }
1987
2001
  });
1988
- app.post("/api/cli-tools/mitm-start", async (_request, reply) => {
1989
- return reply.code(501).send({
1990
- error: "MITM server is not available in Docker deployment mode. Run the proxy directly on the host with root privileges.",
1991
- });
2002
+ app.post("/api/cli-tools/mitm-start", async (request, reply) => {
2003
+ const body = request.body;
2004
+ try {
2005
+ const { generateRootCA, trustRootCA, rootCaExists: caExists } = await import("./mitm/cert.js");
2006
+ const { spawn } = await import("node:child_process");
2007
+ // Ensure CA exists
2008
+ if (!caExists()) {
2009
+ generateRootCA();
2010
+ }
2011
+ // Start MITM server as a child process (needs root for port 443)
2012
+ const mitmScript = path.resolve(import.meta.dirname || __dirname, "mitm", "start-server.js");
2013
+ const sudoPassword = body?.sudoPassword || "";
2014
+ // Use sudo to start the server process
2015
+ const env = {
2016
+ ...process.env,
2017
+ MITM_ROUTER_BASE_URL: `http://127.0.0.1:${config.PORT}`,
2018
+ };
2019
+ const child = spawn("sudo", ["-S", process.execPath, mitmScript], {
2020
+ env,
2021
+ stdio: ["pipe", "ignore", "pipe"],
2022
+ detached: true,
2023
+ });
2024
+ if (sudoPassword && child.stdin) {
2025
+ child.stdin.write(`${sudoPassword}\n`);
2026
+ child.stdin.end();
2027
+ }
2028
+ child.unref();
2029
+ // Wait a moment to check if it started
2030
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2031
+ return reply.send({ ok: true });
2032
+ }
2033
+ catch (error) {
2034
+ return reply.code(500).send({
2035
+ error: error instanceof Error ? error.message : "Failed to start MITM server",
2036
+ });
2037
+ }
1992
2038
  });
1993
2039
  app.post("/api/cli-tools/mitm-stop", async (_request, reply) => {
1994
- return reply.code(501).send({
1995
- error: "MITM server is not available in Docker deployment mode.",
1996
- });
2040
+ try {
2041
+ const { getMitmPid } = await import("./mitm/server.js");
2042
+ const pid = getMitmPid();
2043
+ if (pid) {
2044
+ try {
2045
+ process.kill(pid, "SIGTERM");
2046
+ }
2047
+ catch { /* already dead */ }
2048
+ }
2049
+ return reply.send({ ok: true });
2050
+ }
2051
+ catch (error) {
2052
+ return reply.code(500).send({
2053
+ error: error instanceof Error ? error.message : "Failed to stop MITM server",
2054
+ });
2055
+ }
1997
2056
  });
1998
- app.post("/api/cli-tools/mitm-dns", async (_request, reply) => {
1999
- return reply.code(501).send({
2000
- error: "DNS interception is not available in Docker deployment mode.",
2001
- });
2057
+ app.post("/api/cli-tools/mitm-dns", async (request, reply) => {
2058
+ const body = request.body;
2059
+ const toolId = typeof body?.toolId === "string" ? body.toolId : "";
2060
+ const enable = body?.enable !== false;
2061
+ const sudoPassword = typeof body?.sudoPassword === "string" ? body.sudoPassword : "";
2062
+ try {
2063
+ const { addDNSEntry, removeDNSEntry } = await import("./mitm/dns.js");
2064
+ if (enable) {
2065
+ await addDNSEntry(toolId, sudoPassword);
2066
+ }
2067
+ else {
2068
+ await removeDNSEntry(toolId, sudoPassword);
2069
+ }
2070
+ return reply.send({ ok: true });
2071
+ }
2072
+ catch (error) {
2073
+ return reply.code(500).send({
2074
+ error: error instanceof Error ? error.message : "Failed to update DNS",
2075
+ });
2076
+ }
2077
+ });
2078
+ app.post("/api/cli-tools/mitm-trust-cert", async (request, reply) => {
2079
+ const body = request.body;
2080
+ try {
2081
+ const { generateRootCA, trustRootCA } = await import("./mitm/cert.js");
2082
+ generateRootCA();
2083
+ await trustRootCA(body?.sudoPassword);
2084
+ return reply.send({ ok: true });
2085
+ }
2086
+ catch (error) {
2087
+ return reply.code(500).send({
2088
+ error: error instanceof Error ? error.message : "Failed to trust certificate",
2089
+ });
2090
+ }
2002
2091
  });
2003
2092
  app.get("/api/customer/codex/setup.sh", async (request, reply) => {
2004
2093
  const routingApiKey = readBearerToken(request.headers.authorization);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "responses-proxy",
3
- "version": "0.1.4",
3
+ "version": "0.2.1",
4
4
  "description": "AI routing proxy with multi-provider fallback, RTK token saver, and web dashboard",
5
5
  "bin": {
6
6
  "responses-proxy": "./cli.js"