@miraj181/ipingyou 2.1.9 → 2.1.18
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/README.md +9 -4
- package/SECURITY.md +21 -0
- package/package.json +13 -13
- package/src/cli.js +55 -22
- package/src/lib/ai/safety.js +69 -12
- package/src/lib/broker.js +5 -5
- package/src/lib/chat.js +20 -6
- package/src/lib/checksum.js +22 -2
- package/src/lib/cleanup.js +60 -93
- package/src/lib/crypto.js +27 -0
- package/src/lib/open-url.js +28 -0
- package/src/lib/platform.js +38 -481
- package/src/lib/secure-print.js +7 -1
- package/src/lib/session-log.js +78 -3
- package/src/lib/socket-firewall.js +34 -0
- package/src/lib/ssh.js +32 -6
- package/src/lib/tunnel.js +1 -0
- package/src/lib/uid.js +6 -3
- package/src/lib/worker-runtime.js +81 -0
- package/src/lib/workers/crypto-checksum-worker.js +70 -0
- package/src/modes/ai.js +104 -31
- package/src/modes/client.js +20 -13
- package/src/modes/host.js +316 -116
- package/src/server.js +95 -18
package/src/lib/cleanup.js
CHANGED
|
@@ -3,18 +3,13 @@
|
|
|
3
3
|
* Graceful Cleanup & Process Killer
|
|
4
4
|
* ============================================================
|
|
5
5
|
* Tracks all spawned child processes (cloudflared, ssh, etc.)
|
|
6
|
-
* and kills them on SIGINT/exit
|
|
6
|
+
* and kills them on SIGINT/exit to ensure
|
|
7
7
|
* no orphan processes linger.
|
|
8
8
|
* ============================================================
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import treeKill from 'tree-kill';
|
|
12
11
|
import chalk from 'chalk';
|
|
13
|
-
import fs from 'node:fs';
|
|
14
|
-
import os from 'node:os';
|
|
15
|
-
import path from 'node:path';
|
|
16
12
|
import { execa } from 'execa';
|
|
17
|
-
import { TMUX_SESSION_NAME, TMUX_SESSION_PREFIX, tmuxSocketArgs } from './tmux.js';
|
|
18
13
|
|
|
19
14
|
/** @type {Set<number>} — Active child PIDs we manage */
|
|
20
15
|
const trackedPIDs = new Set();
|
|
@@ -74,15 +69,61 @@ export function setRevokeOnExit(uid, brokerUrl, getHostToken = null) {
|
|
|
74
69
|
* @returns {Promise<void>}
|
|
75
70
|
*/
|
|
76
71
|
export function killProcessTree(pid, signal = 'SIGTERM') {
|
|
77
|
-
return
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
72
|
+
return killProcessTreeSafely(pid, signal);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function killProcessTreeSafely(pid, signal) {
|
|
76
|
+
const rootPid = Number.parseInt(pid, 10);
|
|
77
|
+
if (!Number.isSafeInteger(rootPid) || rootPid <= 0 || rootPid === process.pid) {
|
|
78
|
+
throw new Error('Invalid child process PID');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (process.platform === 'win32') {
|
|
82
|
+
await execa('taskkill', ['/PID', String(rootPid), '/T', '/F'], {
|
|
83
|
+
reject: false,
|
|
84
|
+
timeout: 5000,
|
|
85
|
+
maxBuffer: 64 * 1024,
|
|
84
86
|
});
|
|
85
|
-
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const descendants = [];
|
|
91
|
+
const visited = new Set([rootPid]);
|
|
92
|
+
const pending = [rootPid];
|
|
93
|
+
while (pending.length > 0 && visited.size <= 1024) {
|
|
94
|
+
const parentPid = pending.pop();
|
|
95
|
+
const result = await execa('pgrep', ['-P', String(parentPid)], {
|
|
96
|
+
reject: false,
|
|
97
|
+
timeout: 2000,
|
|
98
|
+
maxBuffer: 64 * 1024,
|
|
99
|
+
}).catch(() => ({ stdout: '' }));
|
|
100
|
+
for (const value of String(result.stdout || '').split(/\s+/)) {
|
|
101
|
+
const childPid = Number.parseInt(value, 10);
|
|
102
|
+
if (!Number.isSafeInteger(childPid) || childPid <= 0 || visited.has(childPid)) continue;
|
|
103
|
+
visited.add(childPid);
|
|
104
|
+
descendants.push(childPid);
|
|
105
|
+
pending.push(childPid);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const targets = [...descendants.reverse(), rootPid];
|
|
110
|
+
for (const targetPid of targets) {
|
|
111
|
+
try {
|
|
112
|
+
process.kill(targetPid, signal);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
if (err.code !== 'ESRCH') throw err;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
119
|
+
for (const targetPid of targets) {
|
|
120
|
+
try {
|
|
121
|
+
process.kill(targetPid, 0);
|
|
122
|
+
process.kill(targetPid, 'SIGKILL');
|
|
123
|
+
} catch (err) {
|
|
124
|
+
if (err.code !== 'ESRCH') throw err;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
86
127
|
}
|
|
87
128
|
|
|
88
129
|
/**
|
|
@@ -174,85 +215,11 @@ export function installShutdownHandlers() {
|
|
|
174
215
|
}
|
|
175
216
|
|
|
176
217
|
/**
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*/
|
|
180
|
-
/**
|
|
181
|
-
* Execute Panic Mode (Self-Destruct)
|
|
182
|
-
* Wipes all configs, keys, and forcefully kills associated processes.
|
|
218
|
+
* Execute a scoped emergency shutdown.
|
|
219
|
+
* Only resources registered by this process are touched.
|
|
183
220
|
*/
|
|
184
221
|
export async function executePanicMode() {
|
|
185
|
-
console.log(chalk.bold.red('\n 🚨 INITIATING
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
console.log(chalk.dim(' [1/4] Terminating all tunnel and host processes...'));
|
|
189
|
-
try {
|
|
190
|
-
if (process.platform === 'win32') {
|
|
191
|
-
// taskkill expects a command string on Windows; pass args safely
|
|
192
|
-
await execa('taskkill', ['/F', '/IM', 'cloudflared.exe'], { reject: false });
|
|
193
|
-
} else {
|
|
194
|
-
// Use argument arrays to avoid shell interpolation
|
|
195
|
-
await execa('pkill', ['-9', '-f', 'cloudflared'], { reject: false });
|
|
196
|
-
await execa('pkill', ['-9', '-f', 'sshd:.*@'], { reject: false });
|
|
197
|
-
|
|
198
|
-
const socketArgs = Array.isArray(tmuxSocketArgs) ? tmuxSocketArgs() : [];
|
|
199
|
-
// Ensure socketArgs are strings and safe-ish before passing to execa
|
|
200
|
-
const safeSocketArgs = (socketArgs || []).map(a => String(a));
|
|
201
|
-
await execa('tmux', [...safeSocketArgs, 'kill-server'], { reject: false });
|
|
202
|
-
|
|
203
|
-
const { stdout } = await execa('tmux', ['list-sessions', '-F', '#{session_name}'], { reject: false });
|
|
204
|
-
const legacyNames = stdout
|
|
205
|
-
.split(/\r?\n/)
|
|
206
|
-
.filter(Boolean)
|
|
207
|
-
.filter(name => name === TMUX_SESSION_NAME || name.startsWith(TMUX_SESSION_PREFIX));
|
|
208
|
-
for (const name of legacyNames) {
|
|
209
|
-
await execa('tmux', ['kill-session', '-t', name], { reject: false });
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
} catch (err) {
|
|
213
|
-
// Best-effort cleanup; log debug info without exposing stack in normal flow
|
|
214
|
-
// (keep behavior unchanged otherwise)
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// 2. Delete configuration and aliases
|
|
218
|
-
console.log(chalk.dim(' [2/4] Wiping configuration files...'));
|
|
219
|
-
const configPath = path.join(os.homedir(), '.ipingyou', 'config.json');
|
|
220
|
-
try {
|
|
221
|
-
if (fs.existsSync(configPath)) {
|
|
222
|
-
fs.unlinkSync(configPath);
|
|
223
|
-
}
|
|
224
|
-
const configDir = path.join(os.homedir(), '.ipingyou');
|
|
225
|
-
if (fs.existsSync(configDir)) {
|
|
226
|
-
fs.rmSync(configDir, { recursive: true, force: true });
|
|
227
|
-
}
|
|
228
|
-
} catch {}
|
|
229
|
-
|
|
230
|
-
// 3. Delete ephemeral keys and temp files
|
|
231
|
-
console.log(chalk.dim(' [3/4] Purging ephemeral keys and temporary files...'));
|
|
232
|
-
try {
|
|
233
|
-
const tmpDir = os.tmpdir();
|
|
234
|
-
const files = fs.readdirSync(tmpDir);
|
|
235
|
-
for (const file of files) {
|
|
236
|
-
if (file.startsWith('ipingyou_') || file.startsWith('ipingyou-')) {
|
|
237
|
-
fs.unlinkSync(path.join(tmpDir, file));
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
} catch {}
|
|
241
|
-
|
|
242
|
-
console.log(chalk.dim(' [4/4] Scrubbing injected SSH keys...'));
|
|
243
|
-
try {
|
|
244
|
-
const authKeysPath = path.join(os.homedir(), '.ssh', 'authorized_keys');
|
|
245
|
-
if (fs.existsSync(authKeysPath)) {
|
|
246
|
-
const current = fs.readFileSync(authKeysPath, 'utf8');
|
|
247
|
-
const cleaned = current
|
|
248
|
-
.split(/\r?\n/)
|
|
249
|
-
.filter(line => !line.includes('ipingyou-ephemeral'))
|
|
250
|
-
.join('\n')
|
|
251
|
-
.replace(/\n{3,}/g, '\n\n');
|
|
252
|
-
if (cleaned !== current) fs.writeFileSync(authKeysPath, cleaned);
|
|
253
|
-
}
|
|
254
|
-
} catch {}
|
|
255
|
-
|
|
256
|
-
console.log(chalk.bold.green('\n ✅ Panic Mode Complete. All traces removed.\n'));
|
|
257
|
-
process.exit(0);
|
|
222
|
+
console.log(chalk.bold.red('\n 🚨 INITIATING SCOPED EMERGENCY SHUTDOWN 🚨\n'));
|
|
223
|
+
await cleanupAll();
|
|
224
|
+
console.log(chalk.bold.green(' ✅ Current iPingYou session stopped safely.\n'));
|
|
258
225
|
}
|
package/src/lib/crypto.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import crypto from 'node:crypto';
|
|
11
|
+
import { canUseWorkers, runWorkerTask } from './worker-runtime.js';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Derive a 256-bit encryption key from a password and salt using PBKDF2.
|
|
@@ -42,6 +43,21 @@ export function encrypt(plaintext, password) {
|
|
|
42
43
|
};
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
export async function encryptAsync(plaintext, password) {
|
|
47
|
+
if (!canUseWorkers()) return encrypt(plaintext, password);
|
|
48
|
+
try {
|
|
49
|
+
const result = await runWorkerTask('encrypt', { plaintext, password });
|
|
50
|
+
return {
|
|
51
|
+
iv: result.iv,
|
|
52
|
+
ciphertext: result.ciphertext,
|
|
53
|
+
salt: result.salt,
|
|
54
|
+
};
|
|
55
|
+
} catch (err) {
|
|
56
|
+
if (err?.code === 'WORKER_QUEUE_FULL') throw err;
|
|
57
|
+
return encrypt(plaintext, password);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
45
61
|
/**
|
|
46
62
|
* Decrypt a ciphertext with AES-256-CBC using a password and salt.
|
|
47
63
|
* @param {string} ivHex — 32-char hex IV
|
|
@@ -60,3 +76,14 @@ export function decrypt(ivHex, cipherBase64, password, saltHex) {
|
|
|
60
76
|
dec += decipher.final('utf8');
|
|
61
77
|
return dec;
|
|
62
78
|
}
|
|
79
|
+
|
|
80
|
+
export async function decryptAsync(ivHex, cipherBase64, password, saltHex) {
|
|
81
|
+
if (!canUseWorkers()) return decrypt(ivHex, cipherBase64, password, saltHex);
|
|
82
|
+
try {
|
|
83
|
+
const result = await runWorkerTask('decrypt', { ivHex, cipherBase64, password, saltHex });
|
|
84
|
+
return result.plaintext;
|
|
85
|
+
} catch (err) {
|
|
86
|
+
if (err?.code === 'WORKER_QUEUE_FULL') throw err;
|
|
87
|
+
return decrypt(ivHex, cipherBase64, password, saltHex);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
|
|
3
|
+
export async function openUrl(value) {
|
|
4
|
+
let url;
|
|
5
|
+
try {
|
|
6
|
+
url = new URL(String(value));
|
|
7
|
+
} catch {
|
|
8
|
+
throw new Error('Cannot open an invalid URL');
|
|
9
|
+
}
|
|
10
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.href.length > 4096) {
|
|
11
|
+
throw new Error('Only HTTP(S) URLs can be opened');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const [command, args] = process.platform === 'darwin'
|
|
15
|
+
? ['open', [url.href]]
|
|
16
|
+
: process.platform === 'win32'
|
|
17
|
+
? ['explorer.exe', [url.href]]
|
|
18
|
+
: ['xdg-open', [url.href]];
|
|
19
|
+
|
|
20
|
+
const result = await execa(command, args, {
|
|
21
|
+
reject: false,
|
|
22
|
+
stdio: 'ignore',
|
|
23
|
+
timeout: 10000,
|
|
24
|
+
});
|
|
25
|
+
if (result.failed && result.exitCode !== 0) {
|
|
26
|
+
throw new Error(`Could not open URL with ${command}`);
|
|
27
|
+
}
|
|
28
|
+
}
|