@miraj181/ipingyou 2.1.15 → 2.1.19
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 -16
- package/src/cli.js +33 -3
- package/src/lib/ai/safety.js +69 -12
- package/src/lib/broker.js +1 -1
- package/src/lib/chat.js +13 -5
- package/src/lib/cleanup.js +60 -93
- package/src/lib/open-url.js +28 -0
- package/src/lib/platform.js +38 -481
- package/src/lib/socket-firewall.js +34 -0
- package/src/lib/ssh.js +7 -1
- package/src/lib/uid.js +6 -3
- package/src/modes/ai.js +102 -26
- package/src/modes/client.js +12 -3
- package/src/modes/host.js +168 -99
- package/src/server.js +5 -2
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ No firewalls to configure. No port forwarding. No plaintext leakage.
|
|
|
20
20
|
* 📺 **Terminal Mirroring**: Wrap client SSH sessions in a multiplexed `tmux` terminal. The Host can spectate connected clients in real-time right from the dashboard to audit or assist.
|
|
21
21
|
* 🔄 **Reverse Port Forwarding (`ssh -R`)**: Clients can expose their *local* `localhost` development ports back to the Host through the secure tunnel.
|
|
22
22
|
* 📡 **Hardware Telemetry Verification**: Clients silently generate hardware footprint reports (OS, RAM, CPU, IP), encrypt them locally with the session password, and send them to the Host for authorization.
|
|
23
|
-
* 🚨 **
|
|
23
|
+
* 🚨 **Scoped Emergency Stop**: Type `ipingyou panic` and confirm locally to stop only processes and temporary credentials owned by the current iPingYou session.
|
|
24
24
|
* 👻 **Daemonization**: Run `ipingyou service install` to quietly install and run the Host listener in the background (survives system reboots using PM2).
|
|
25
25
|
* 🧭 **Approval Gate**: Require the Host to explicitly approve clients before they receive tunnel/key material.
|
|
26
26
|
* 📦 **One-Time File Share**: Serve a single file/folder over SCP and revoke after use.
|
|
@@ -48,12 +48,17 @@ npx @miraj181/ipingyou connect
|
|
|
48
48
|
|
|
49
49
|
### Global Install
|
|
50
50
|
```bash
|
|
51
|
-
|
|
51
|
+
# Install Socket Firewall once, then scan iPingYou before installation
|
|
52
|
+
npm install -g sfw
|
|
53
|
+
sfw npm install -g @miraj181/ipingyou
|
|
52
54
|
|
|
53
55
|
# Execute globally using aliases:
|
|
54
56
|
ipingyou
|
|
55
57
|
# or
|
|
56
58
|
securelink
|
|
59
|
+
|
|
60
|
+
# Verify future in-app npm installs are firewall-protected
|
|
61
|
+
ipingyou security-status
|
|
57
62
|
```
|
|
58
63
|
|
|
59
64
|
---
|
|
@@ -102,7 +107,7 @@ sequenceDiagram
|
|
|
102
107
|
|
|
103
108
|
## 🛡️ Security Scanner Disclaimer
|
|
104
109
|
|
|
105
|
-
Because **iPingYou** is a
|
|
110
|
+
Because **iPingYou** is a remote-access tool with background daemonization and SSH process management, automated security scanners may classify it as security-sensitive. Native dependencies are never downloaded or installed automatically, and emergency cleanup is limited to resources registered by the current process.
|
|
106
111
|
|
|
107
112
|
These alerts (e.g., "AI-detected potential code anomaly", "Shell access", "Network access") are **expected behavior** for a peer-to-peer tunneling utility. The source code is entirely open-source, heavily documented, and uses zero-knowledge encryption to ensure your data is safe.
|
|
108
113
|
|
|
@@ -128,7 +133,7 @@ These alerts (e.g., "AI-detected potential code anomaly", "Shell access", "Netwo
|
|
|
128
133
|
| `ipingyou connect -u <UID>` | Connect directly to a specific UID. |
|
|
129
134
|
| `ipingyou ai` | Groq-powered task assistant with guarded local/remote tools. |
|
|
130
135
|
| `ipingyou doctor` | Diagnostics for dependencies, SSH, broker, SCP, AI, and tests. |
|
|
131
|
-
| `ipingyou panic` | 🚨
|
|
136
|
+
| `ipingyou panic` | 🚨 Confirmed emergency stop for the current iPingYou session only. |
|
|
132
137
|
| `ipingyou service install` | 👻 Installs Host mode as an always-on background daemon. |
|
|
133
138
|
| `ipingyou service stop` | Stops and removes the background daemon. |
|
|
134
139
|
| `ipingyou service status` | Shows background daemon status. |
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
Use this section to tell people about which versions of your project are
|
|
6
|
+
currently being supported with security updates.
|
|
7
|
+
|
|
8
|
+
| Version | Supported |
|
|
9
|
+
| ------- | ------------------ |
|
|
10
|
+
| 5.1.x | :white_check_mark: |
|
|
11
|
+
| 5.0.x | :x: |
|
|
12
|
+
| 4.0.x | :white_check_mark: |
|
|
13
|
+
| < 4.0 | :x: |
|
|
14
|
+
|
|
15
|
+
## Reporting a Vulnerability
|
|
16
|
+
|
|
17
|
+
Use this section to tell people how to report a vulnerability.
|
|
18
|
+
|
|
19
|
+
Tell them where to go, how often they can expect to get an update on a
|
|
20
|
+
reported vulnerability, what to expect if the vulnerability is accepted or
|
|
21
|
+
declined, etc.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miraj181/ipingyou",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.19",
|
|
4
4
|
"description": "SecureLink-CLI — Secure peer-to-peer remote access via SSH & Cloudflare Tunnels",
|
|
5
5
|
"main": "src/cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -13,12 +13,13 @@
|
|
|
13
13
|
"src/lib/",
|
|
14
14
|
"src/modes/",
|
|
15
15
|
"README.md",
|
|
16
|
+
"SECURITY.md",
|
|
16
17
|
"LICENSE"
|
|
17
18
|
],
|
|
18
19
|
"scripts": {
|
|
19
20
|
"start": "node src/server.js",
|
|
20
21
|
"dev:cli": "node src/cli.js",
|
|
21
|
-
"test": "node test/test_ai_helpers.js",
|
|
22
|
+
"test": "node test/test_ai_helpers.js && node test/test_security_static.js && node test/test_e2e_crypto.js",
|
|
22
23
|
"prepublishOnly": "node -e \"require('fs').accessSync('LICENSE')\" && node src/cli.js --help"
|
|
23
24
|
},
|
|
24
25
|
"keywords": [
|
|
@@ -44,25 +45,21 @@
|
|
|
44
45
|
"url": "https://github.com/skmirajulislam/ipingyou/issues"
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
47
|
-
"chalk": "
|
|
48
|
-
"commander": "
|
|
49
|
-
"express": "
|
|
50
|
-
"express-rate-limit": "
|
|
51
|
-
"execa": "
|
|
52
|
-
"helmet": "
|
|
53
|
-
"inquirer": "
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"ora": "^9.4.0",
|
|
57
|
-
"shell-quote": "^1.8.4",
|
|
58
|
-
"tree-kill": "^1.2.2",
|
|
59
|
-
"ws": "^8.20.1"
|
|
48
|
+
"chalk": "5.6.2",
|
|
49
|
+
"commander": "15.0.0",
|
|
50
|
+
"express": "4.22.2",
|
|
51
|
+
"express-rate-limit": "7.5.1",
|
|
52
|
+
"execa": "9.6.1",
|
|
53
|
+
"helmet": "8.2.0",
|
|
54
|
+
"inquirer": "12.11.1",
|
|
55
|
+
"ora": "9.4.1",
|
|
56
|
+
"ws": "8.21.0"
|
|
60
57
|
},
|
|
61
58
|
"devDependencies": {
|
|
62
59
|
"nodemon": "^3.1.4"
|
|
63
60
|
},
|
|
64
61
|
"engines": {
|
|
65
|
-
"node": ">=
|
|
62
|
+
"node": ">=22.12.0"
|
|
66
63
|
},
|
|
67
64
|
"type": "module"
|
|
68
65
|
}
|
package/src/cli.js
CHANGED
|
@@ -29,6 +29,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
29
29
|
import { detectOS, checkDependencies } from './lib/platform.js';
|
|
30
30
|
import { cleanupAll, installShutdownHandlers, executePanicMode } from './lib/cleanup.js';
|
|
31
31
|
import { cleanupSessionLog } from './lib/session-log.js';
|
|
32
|
+
import { getSocketFirewallStatus, runProtectedNpmInstall } from './lib/socket-firewall.js';
|
|
32
33
|
import { startHostMode } from './modes/host.js';
|
|
33
34
|
import { startClientMode } from './modes/client.js';
|
|
34
35
|
import { startAIMode } from './modes/ai.js';
|
|
@@ -330,10 +331,19 @@ program
|
|
|
330
331
|
|
|
331
332
|
program
|
|
332
333
|
.command('panic')
|
|
333
|
-
.description('🚨
|
|
334
|
+
.description('🚨 Stop resources owned by the current iPingYou session')
|
|
334
335
|
.action(async () => {
|
|
335
336
|
try {
|
|
336
337
|
showBanner();
|
|
338
|
+
const { confirmation } = await inquirer.prompt([{
|
|
339
|
+
type: 'input',
|
|
340
|
+
name: 'confirmation',
|
|
341
|
+
message: 'Type STOP CURRENT SESSION to confirm:',
|
|
342
|
+
}]);
|
|
343
|
+
if (confirmation !== 'STOP CURRENT SESSION') {
|
|
344
|
+
console.log(chalk.yellow(' Emergency shutdown cancelled.'));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
337
347
|
await executePanicMode();
|
|
338
348
|
} catch (err) {
|
|
339
349
|
fatal('panic', err);
|
|
@@ -352,8 +362,8 @@ program
|
|
|
352
362
|
const { execa } = await import('execa');
|
|
353
363
|
|
|
354
364
|
if (action === 'install') {
|
|
355
|
-
console.log(chalk.dim('
|
|
356
|
-
await
|
|
365
|
+
console.log(chalk.dim(' Scanning and installing PM2 through Socket Firewall...'));
|
|
366
|
+
await runProtectedNpmInstall(['-g', 'pm2'], { stdio: 'inherit' });
|
|
357
367
|
await execa('pm2', ['start', 'ipingyou', '--name', 'ipingyou-host', '--', 'host'], { stdio: 'inherit' });
|
|
358
368
|
await execa('pm2', ['save'], { stdio: 'inherit' });
|
|
359
369
|
await execa('pm2', ['startup'], { stdio: 'inherit' });
|
|
@@ -373,6 +383,26 @@ program
|
|
|
373
383
|
}
|
|
374
384
|
});
|
|
375
385
|
|
|
386
|
+
program
|
|
387
|
+
.command('security-status')
|
|
388
|
+
.description('Check Socket Firewall protection for future npm installations')
|
|
389
|
+
.action(async () => {
|
|
390
|
+
try {
|
|
391
|
+
showBanner();
|
|
392
|
+
const status = await getSocketFirewallStatus();
|
|
393
|
+
if (!status.available) {
|
|
394
|
+
console.log(chalk.yellow(' ⚠️ Socket Firewall is not available.'));
|
|
395
|
+
console.log(chalk.dim(' Install it with: npm install -g sfw'));
|
|
396
|
+
process.exitCode = 1;
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
console.log(chalk.green(` ✅ Socket Firewall active${status.version ? ` (${status.version})` : ''}`));
|
|
400
|
+
console.log(chalk.dim(' In-app npm installations are routed through: sfw npm install'));
|
|
401
|
+
} catch (err) {
|
|
402
|
+
fatal('security-status', err);
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
376
406
|
program
|
|
377
407
|
.command('allowlist')
|
|
378
408
|
.description('Manage AI command allowlist — add, remove, or list safe regex patterns')
|
package/src/lib/ai/safety.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getAllowlistRegexes } from '../allowlist.js';
|
|
2
|
+
|
|
1
3
|
const SECRET_PATTERNS = [
|
|
2
4
|
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
3
5
|
/\b(gsk_[A-Za-z0-9_-]{20,})\b/g,
|
|
@@ -20,6 +22,31 @@ const BLOCKED_PATH_PATTERNS = [
|
|
|
20
22
|
/id_(rsa|dsa|ecdsa|ed25519)(\.pub)?$/,
|
|
21
23
|
];
|
|
22
24
|
|
|
25
|
+
const BLOCKED_PATH_SEGMENTS = new Set([
|
|
26
|
+
'.ssh',
|
|
27
|
+
'.gnupg',
|
|
28
|
+
'.aws',
|
|
29
|
+
'.ipingyou',
|
|
30
|
+
'.kube',
|
|
31
|
+
'.docker',
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const BLOCKED_PATH_BASENAMES = new Set([
|
|
35
|
+
'.npmrc',
|
|
36
|
+
'.netrc',
|
|
37
|
+
'.pypirc',
|
|
38
|
+
'credentials',
|
|
39
|
+
'credentials.json',
|
|
40
|
+
'known_hosts',
|
|
41
|
+
'authorized_keys',
|
|
42
|
+
'shadow',
|
|
43
|
+
'sudoers',
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
const PRIVATE_KEY_BASENAME = /^id_(rsa|dsa|ecdsa|ed25519)(\.pub)?$/i;
|
|
47
|
+
const ENV_BASENAME = /^\.env(?:\.|$)/i;
|
|
48
|
+
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
|
|
49
|
+
|
|
23
50
|
const BLOCKED_COMMAND_PATTERNS = [
|
|
24
51
|
/^\s*(env|printenv|set)\b/,
|
|
25
52
|
/\b(export|declare)\s+-p\b/,
|
|
@@ -41,8 +68,7 @@ const DANGEROUS_COMMAND_PATTERNS = [
|
|
|
41
68
|
/\b(curl|wget)\b/,
|
|
42
69
|
/>|>>|\btee\b/,
|
|
43
70
|
];
|
|
44
|
-
|
|
45
|
-
import { getAllowlistRegexes } from '../allowlist.js';
|
|
71
|
+
const SHELL_SYNTAX_PATTERN = /(?:[;&|`$()]|\\\r?\n)/;
|
|
46
72
|
|
|
47
73
|
const READ_ONLY_COMMAND_PATTERNS = [
|
|
48
74
|
/^\s*(pwd|ls|find|rg|grep|sed|cat|head|tail|wc|git status|git diff|git log|git show|node --version|npm --version|which|date|uname)\b/,
|
|
@@ -66,6 +92,33 @@ export function commandTouchesBlockedPath(command) {
|
|
|
66
92
|
return BLOCKED_PATH_PATTERNS.some(pattern => pattern.test(command));
|
|
67
93
|
}
|
|
68
94
|
|
|
95
|
+
export function assertSafeReadablePath(filePath) {
|
|
96
|
+
const value = String(filePath ?? '');
|
|
97
|
+
if (!value || value.length > 4096) {
|
|
98
|
+
throw new Error('Invalid file path');
|
|
99
|
+
}
|
|
100
|
+
if (CONTROL_CHARACTERS.test(value)) {
|
|
101
|
+
throw new Error('File path contains control characters');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const normalized = value.replace(/\\/g, '/').replace(/\/+/g, '/');
|
|
105
|
+
const segments = normalized.toLowerCase().split('/').filter(Boolean);
|
|
106
|
+
const basename = segments.at(-1) || '';
|
|
107
|
+
|
|
108
|
+
if (
|
|
109
|
+
segments.some(segment => BLOCKED_PATH_SEGMENTS.has(segment))
|
|
110
|
+
|| BLOCKED_PATH_BASENAMES.has(basename)
|
|
111
|
+
|| PRIVATE_KEY_BASENAME.test(basename)
|
|
112
|
+
|| ENV_BASENAME.test(basename)
|
|
113
|
+
|| normalized.toLowerCase().includes('/.config/gh/')
|
|
114
|
+
|| normalized.toLowerCase().includes('/proc/') && basename === 'environ'
|
|
115
|
+
) {
|
|
116
|
+
throw new Error('Path references a protected secret/config location');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
|
|
69
122
|
export function classifyCommand(command) {
|
|
70
123
|
const text = String(command || '').trim();
|
|
71
124
|
if (!text) return { blocked: true, needsApproval: true, reason: 'Empty command' };
|
|
@@ -78,16 +131,6 @@ export function classifyCommand(command) {
|
|
|
78
131
|
};
|
|
79
132
|
}
|
|
80
133
|
|
|
81
|
-
// Check user-provided allowlist first (if present)
|
|
82
|
-
try {
|
|
83
|
-
const userPatterns = getAllowlistRegexes();
|
|
84
|
-
if (Array.isArray(userPatterns) && userPatterns.some(p => p.test(text))) {
|
|
85
|
-
return { blocked: false, needsApproval: false, reason: 'Matched user allowlist' };
|
|
86
|
-
}
|
|
87
|
-
} catch {
|
|
88
|
-
// ignore allowlist errors and fall back to defaults
|
|
89
|
-
}
|
|
90
|
-
|
|
91
134
|
if (BLOCKED_COMMAND_PATTERNS.some(pattern => pattern.test(text))) {
|
|
92
135
|
return {
|
|
93
136
|
blocked: true,
|
|
@@ -100,6 +143,20 @@ export function classifyCommand(command) {
|
|
|
100
143
|
return { blocked: false, needsApproval: true, reason: 'Command may modify files, install packages, or access the network' };
|
|
101
144
|
}
|
|
102
145
|
|
|
146
|
+
if (SHELL_SYNTAX_PATTERN.test(text)) {
|
|
147
|
+
return { blocked: false, needsApproval: true, reason: 'Command uses shell syntax and requires explicit approval' };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// User allowlists may suppress prompts only after non-bypassable safety checks.
|
|
151
|
+
try {
|
|
152
|
+
const userPatterns = getAllowlistRegexes();
|
|
153
|
+
if (Array.isArray(userPatterns) && userPatterns.some(pattern => pattern.test(text))) {
|
|
154
|
+
return { blocked: false, needsApproval: false, reason: 'Matched user allowlist' };
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
// Ignore allowlist errors and fall back to defaults.
|
|
158
|
+
}
|
|
159
|
+
|
|
103
160
|
if (READ_ONLY_COMMAND_PATTERNS.some(pattern => pattern.test(text))) {
|
|
104
161
|
return { blocked: false, needsApproval: false, reason: 'Read-only command' };
|
|
105
162
|
}
|
package/src/lib/broker.js
CHANGED
|
@@ -264,7 +264,7 @@ export async function pushTelemetry(brokerUrl, uid, password, username, action =
|
|
|
264
264
|
os: `${os.type()} ${os.release()} (${os.arch()})`,
|
|
265
265
|
// Limit fingerprint detail to reduce privacy surface
|
|
266
266
|
cpu: os.cpus()[0]?.model ? os.cpus()[0].model.replace(/\s{2,}/g, ' ').trim() : 'Unknown',
|
|
267
|
-
|
|
267
|
+
ram: Math.round(os.totalmem() / 1024 / 1024 / 1024) + ' GB',
|
|
268
268
|
action,
|
|
269
269
|
time: new Date().toISOString()
|
|
270
270
|
};
|
package/src/lib/chat.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import { WebSocketServer } from 'ws';
|
|
3
|
-
import
|
|
3
|
+
import { openUrl } from './open-url.js';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { secureSensitiveUrl } from './secure-print.js';
|
|
6
6
|
|
|
@@ -97,9 +97,17 @@ const HTML_CONTENT = `
|
|
|
97
97
|
let username = isHost ? 'Host' : prompt('Enter your name for the chat:', 'Client_' + Math.floor(Math.random()*1000));
|
|
98
98
|
if (!username) username = 'Anonymous';
|
|
99
99
|
|
|
100
|
+
function showBodyMessage(text, color) {
|
|
101
|
+
const message = document.createElement('h2');
|
|
102
|
+
message.style.cssText = 'text-align:center;margin-top:20vh';
|
|
103
|
+
if (color) message.style.color = color;
|
|
104
|
+
message.textContent = text;
|
|
105
|
+
document.body.replaceChildren(message);
|
|
106
|
+
}
|
|
107
|
+
|
|
100
108
|
const sessionPassword = window.location.hash.substring(1);
|
|
101
109
|
if (!sessionPassword) {
|
|
102
|
-
|
|
110
|
+
showBodyMessage('Fatal: Missing session password in URL hash. Cannot decrypt E2E chat.', 'red');
|
|
103
111
|
throw new Error("Missing password");
|
|
104
112
|
}
|
|
105
113
|
|
|
@@ -181,7 +189,7 @@ const HTML_CONTENT = `
|
|
|
181
189
|
}
|
|
182
190
|
|
|
183
191
|
function updateUsers(users) {
|
|
184
|
-
usersList.
|
|
192
|
+
usersList.replaceChildren();
|
|
185
193
|
users.forEach(u => {
|
|
186
194
|
const li = document.createElement('li');
|
|
187
195
|
li.className = 'user-item';
|
|
@@ -252,7 +260,7 @@ const HTML_CONTENT = `
|
|
|
252
260
|
} else {
|
|
253
261
|
ws.close();
|
|
254
262
|
window.close();
|
|
255
|
-
|
|
263
|
+
showBodyMessage('You have left the chat. You can close this tab.');
|
|
256
264
|
}
|
|
257
265
|
};
|
|
258
266
|
</script>
|
|
@@ -327,7 +335,7 @@ export async function startChatServer(onClose) {
|
|
|
327
335
|
export async function openLocalChatUI(port, password) {
|
|
328
336
|
try {
|
|
329
337
|
const chatUrl = `http://localhost:${port}#${password}`;
|
|
330
|
-
await
|
|
338
|
+
await openUrl(chatUrl);
|
|
331
339
|
} catch {
|
|
332
340
|
console.log(chalk.dim(` Unable to auto-open browser. Visit ${secureSensitiveUrl(`http://localhost:${port}`, password)}`));
|
|
333
341
|
}
|
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
|
}
|
|
@@ -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
|
+
}
|