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.
- package/cli.js +211 -26
- package/mcp-server.cjs +432 -570
- 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>
|
|
523
|
-
console.log("
|
|
524
|
-
console.log("
|
|
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
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
}
|