iframer-cli 2.1.2 → 2.1.5

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.
Files changed (3) hide show
  1. package/cli.js +211 -26
  2. package/mcp-server.cjs +432 -570
  3. package/package.json +1 -1
package/cli.js CHANGED
@@ -7,10 +7,182 @@ const { execSync } = require("child_process");
7
7
 
8
8
  const readline = require("readline");
9
9
 
10
+ const net = require("net");
11
+ const { spawn } = require("child_process");
12
+
10
13
  const CONFIG_DIR = path.join(require("os").homedir(), ".iframer");
11
14
  const CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json");
15
+ const PROXY_FILE = path.join(CONFIG_DIR, "proxy.json");
12
16
  const DEFAULT_SERVER = "https://api.iframer.sh";
13
17
 
18
+ // ─── SOCKS5 server ───────────────────────────────────────────────────────────
19
+
20
+ function createSocks5Server() {
21
+ function handleClient(socket) {
22
+ socket.on("error", () => socket.destroy());
23
+ socket.once("data", (greeting) => {
24
+ if (greeting[0] !== 5) { socket.destroy(); return; }
25
+ socket.write(Buffer.from([5, 0]));
26
+ socket.once("data", (req) => {
27
+ if (req[0] !== 5 || req[1] !== 1) { socket.destroy(); return; }
28
+ let host, port;
29
+ const atype = req[3];
30
+ if (atype === 1) {
31
+ host = `${req[4]}.${req[5]}.${req[6]}.${req[7]}`;
32
+ port = req.readUInt16BE(8);
33
+ } else if (atype === 3) {
34
+ const len = req[4];
35
+ host = req.slice(5, 5 + len).toString();
36
+ port = req.readUInt16BE(5 + len);
37
+ } else if (atype === 4) {
38
+ const parts = [];
39
+ for (let i = 0; i < 16; i += 2) parts.push(req.readUInt16BE(4 + i).toString(16));
40
+ host = parts.join(":");
41
+ port = req.readUInt16BE(20);
42
+ } else { socket.destroy(); return; }
43
+ const target = net.connect(port, host, () => {
44
+ const reply = Buffer.alloc(10);
45
+ reply[0] = 5; reply[1] = 0; reply[2] = 0; reply[3] = 1;
46
+ socket.write(reply);
47
+ socket.pipe(target);
48
+ target.pipe(socket);
49
+ });
50
+ target.on("error", () => socket.destroy());
51
+ socket.on("close", () => target.destroy());
52
+ });
53
+ });
54
+ }
55
+ return net.createServer(handleClient);
56
+ }
57
+
58
+ function startSocks5(listenAll) {
59
+ return new Promise((resolve, reject) => {
60
+ const server = createSocks5Server();
61
+ server.listen(0, listenAll ? "0.0.0.0" : "127.0.0.1", () => {
62
+ resolve({ server, port: server.address().port });
63
+ });
64
+ server.on("error", reject);
65
+ });
66
+ }
67
+
68
+ // ─── WebSocket tunnel client ─────────────────────────────────────────────────
69
+
70
+ function startWsTunnel(baseUrl, authToken, localSocksPort) {
71
+ return new Promise((resolve, reject) => {
72
+ if (!globalThis.WebSocket) {
73
+ reject(new Error("Node.js 22+ is required for the home proxy tunnel. Please upgrade Node."));
74
+ return;
75
+ }
76
+ const wsUrl = baseUrl.replace(/^http/, "ws") + "/proxy/tunnel";
77
+ const ws = new globalThis.WebSocket(wsUrl);
78
+ const sockets = new Map();
79
+ let done = false;
80
+
81
+ const fail = (reason) => {
82
+ if (!done) { done = true; reject(new Error(reason)); }
83
+ try { ws.close(); } catch {}
84
+ };
85
+
86
+ ws.onopen = () => {
87
+ try { ws.send(JSON.stringify({ type: "auth", token: authToken })); }
88
+ catch (e) { fail(`Auth failed: ${e.message}`); }
89
+ };
90
+
91
+ ws.onmessage = (event) => {
92
+ try {
93
+ const msg = JSON.parse(event.data);
94
+ if (msg.type === "ready") {
95
+ if (!done) { done = true; resolve({ remotePort: msg.port, ws, sockets }); }
96
+ return;
97
+ }
98
+ if (msg.type === "open") {
99
+ const sock = net.connect(localSocksPort, "127.0.0.1");
100
+ sockets.set(msg.id, sock);
101
+ sock.on("data", (d) => {
102
+ try { if (ws.readyState === 1) ws.send(JSON.stringify({ type: "data", id: msg.id, data: d.toString("base64") })); }
103
+ catch {}
104
+ });
105
+ sock.on("close", () => {
106
+ sockets.delete(msg.id);
107
+ try { if (ws.readyState === 1) ws.send(JSON.stringify({ type: "close", id: msg.id })); } catch {}
108
+ });
109
+ sock.on("error", () => sock.destroy());
110
+ return;
111
+ }
112
+ if (msg.type === "data") { sockets.get(msg.id)?.write(Buffer.from(msg.data, "base64")); return; }
113
+ if (msg.type === "close") { sockets.get(msg.id)?.destroy(); sockets.delete(msg.id); }
114
+ } catch {}
115
+ };
116
+
117
+ ws.onerror = (e) => fail(`WebSocket error: ${e?.message ?? "unknown"}`);
118
+ ws.onclose = (e) => {
119
+ sockets.forEach(s => s.destroy()); sockets.clear();
120
+ fail(`Tunnel closed (code=${e?.code ?? "?"}, reason=${e?.reason || "unknown"})`);
121
+ };
122
+
123
+ setTimeout(() => fail("Tunnel setup timed out (15s)"), 15000);
124
+ });
125
+ }
126
+
127
+ // ─── Proxy daemon ────────────────────────────────────────────────────────────
128
+
129
+ function readProxyState() {
130
+ try {
131
+ const data = JSON.parse(fs.readFileSync(PROXY_FILE, "utf8"));
132
+ try { process.kill(data.pid, 0); return { active: true, ...data }; }
133
+ catch { try { fs.unlinkSync(PROXY_FILE); } catch {} return { active: false }; }
134
+ } catch { return { active: false }; }
135
+ }
136
+
137
+ async function runProxyDaemon() {
138
+ const server = getServer();
139
+ const isLocal = server.includes("localhost") || server.includes("127.0.0.1");
140
+
141
+ let socks, tunnel;
142
+ const cleanup = async () => {
143
+ try { socks?.server.close(); } catch {}
144
+ try { tunnel?.ws.close(); } catch {}
145
+ try {
146
+ const ac = new AbortController();
147
+ setTimeout(() => ac.abort(), 3000);
148
+ await fetch(`${server}/proxy`, {
149
+ method: "POST", headers: { "Content-Type": "application/json" },
150
+ body: JSON.stringify({ server: null }),
151
+ signal: ac.signal,
152
+ });
153
+ } catch {}
154
+ try { fs.unlinkSync(PROXY_FILE); } catch {}
155
+ };
156
+
157
+ try {
158
+ socks = await startSocks5(true); // listen on 0.0.0.0 so Docker can reach it
159
+
160
+ let proxyUrl;
161
+ if (isLocal) {
162
+ proxyUrl = `socks5://host.docker.internal:${socks.port}`;
163
+ } else {
164
+ const token = loadToken();
165
+ tunnel = await startWsTunnel(server, token, socks.port);
166
+ proxyUrl = `socks5://127.0.0.1:${tunnel.remotePort}`;
167
+ }
168
+
169
+ await fetch(`${server}/proxy`, {
170
+ method: "POST", headers: { "Content-Type": "application/json" },
171
+ body: JSON.stringify({ server: proxyUrl }),
172
+ });
173
+
174
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
175
+ fs.writeFileSync(PROXY_FILE, JSON.stringify({ pid: process.pid, proxyUrl, startedAt: new Date().toISOString() }, null, 2));
176
+
177
+ await new Promise((resolve) => {
178
+ process.once("SIGTERM", resolve);
179
+ process.once("SIGINT", resolve);
180
+ });
181
+ } finally {
182
+ await cleanup();
183
+ }
184
+ }
185
+
14
186
  function getServer() {
15
187
  return process.env.IFRAMER_URL || DEFAULT_SERVER;
16
188
  }
@@ -518,38 +690,51 @@ async function main() {
518
690
 
519
691
  case "proxy": {
520
692
  const action = args[0];
693
+ const isDaemon = args.includes("--daemon");
694
+ const isForeground = args.includes("--foreground");
695
+
521
696
  if (!action || !["start", "stop", "status"].includes(action)) {
522
- console.log(" Usage: iframer proxy <start|stop|status> [proxy-url]");
523
- console.log(" Note: 'start' requires a proxy URL (e.g. socks5://host:port).");
524
- console.log(" To use your home IP automatically, use the iframer MCP tool instead.");
697
+ console.log(" Usage: iframer proxy <start|stop|status>");
698
+ console.log(" start Route browser traffic through your home IP (runs in foreground)");
699
+ console.log(" stop Stop the home proxy");
700
+ console.log(" status Check proxy status");
525
701
  break;
526
702
  }
703
+
527
704
  if (action === "status") {
528
- const res = await fetch(`${getServer()}/proxy`);
529
- const data = await res.json();
530
- console.log(JSON.stringify(data, null, 2));
531
- } else if (action === "stop") {
532
- const res = await fetch(`${getServer()}/proxy`, {
533
- method: "POST",
534
- headers: { "Content-Type": "application/json" },
535
- body: JSON.stringify({ server: null }),
536
- });
537
- const data = await res.json();
538
- console.log(JSON.stringify(data, null, 2));
539
- } else if (action === "start") {
540
- const proxyUrl = args[1];
541
- if (!proxyUrl) {
542
- console.error(" Error: 'proxy start' requires a proxy URL, e.g.: iframer proxy start socks5://host:port");
543
- console.error(" To route through your home IP automatically, use the iframer MCP tool.");
705
+ console.log(JSON.stringify(readProxyState(), null, 2));
706
+ break;
707
+ }
708
+
709
+ if (action === "stop") {
710
+ const state = readProxyState();
711
+ if (!state.active) { console.log(JSON.stringify({ ok: true, message: "No proxy running" })); break; }
712
+ try { process.kill(state.pid, "SIGTERM"); } catch {}
713
+ await new Promise(r => setTimeout(r, 1500));
714
+ console.log(JSON.stringify({ ok: true, message: "Proxy stopped" }));
715
+ break;
716
+ }
717
+
718
+ if (action === "start") {
719
+ if (isDaemon) {
720
+ // Fork self with --foreground, detach, wait for proxy.json
721
+ const child = spawn(process.execPath, [__filename, "proxy", "start", "--foreground"], {
722
+ detached: true, stdio: "ignore", env: process.env,
723
+ });
724
+ child.unref();
725
+ const deadline = Date.now() + 20000;
726
+ while (Date.now() < deadline) {
727
+ await new Promise(r => setTimeout(r, 500));
728
+ const state = readProxyState();
729
+ if (state.active) { console.log(JSON.stringify({ ok: true, ...state })); process.exit(0); }
730
+ }
731
+ console.error(JSON.stringify({ ok: false, error: "Proxy daemon failed to start within 20s" }));
544
732
  process.exit(1);
545
733
  }
546
- const res = await fetch(`${getServer()}/proxy`, {
547
- method: "POST",
548
- headers: { "Content-Type": "application/json" },
549
- body: JSON.stringify({ server: proxyUrl }),
550
- });
551
- const data = await res.json();
552
- console.log(JSON.stringify(data, null, 2));
734
+
735
+ // Foreground mode (both --foreground and plain start)
736
+ await runProxyDaemon();
737
+ break;
553
738
  }
554
739
  break;
555
740
  }