@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.
@@ -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 using tree-kill to ensure
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 new Promise((resolve) => {
78
- treeKill(pid, signal, (err) => {
79
- if (err) {
80
- treeKill(pid, 'SIGKILL', () => resolve());
81
- } else {
82
- resolve();
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
- * Get count of tracked PIDs.
178
- * @returns {number}
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 SECURELINK PANIC MODE 🚨\n'));
186
-
187
- // 1. Force kill all cloudflared & ipingyou processes
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
+ }