@miraj181/ipingyou 2.1.19 → 2.1.23
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 +1 -2
- package/package.json +1 -1
- package/src/cli.js +22 -14
- package/src/lib/ai/safety.js +1 -1
- package/src/lib/{broker.js → client/broker.js} +28 -12
- package/src/lib/{path-browser.js → client/path-browser.js} +6 -2
- package/src/lib/{session-log.js → mod/session-log.js} +17 -8
- package/src/lib/{worker-runtime.js → mod/worker-runtime.js} +1 -1
- package/src/lib/{chat.js → services/chat.js} +2 -2
- package/src/lib/services/platform.js +320 -0
- package/src/lib/{ssh.js → services/ssh.js} +14 -1
- package/src/lib/{tunnel.js → services/tunnel.js} +5 -3
- package/src/modes/ai.js +18 -9
- package/src/modes/client.js +103 -25
- package/src/modes/doctor.js +7 -8
- package/src/modes/host.js +322 -157
- package/src/server.js +50 -4
- package/src/lib/platform.js +0 -90
- /package/src/lib/{allowlist.js → mod/allowlist.js} +0 -0
- /package/src/lib/{animations.js → mod/animations.js} +0 -0
- /package/src/lib/{checksum.js → mod/checksum.js} +0 -0
- /package/src/lib/{cleanup.js → mod/cleanup.js} +0 -0
- /package/src/lib/{config.js → mod/config.js} +0 -0
- /package/src/lib/{crypto.js → mod/crypto.js} +0 -0
- /package/src/lib/{open-url.js → mod/open-url.js} +0 -0
- /package/src/lib/{secure-print.js → mod/secure-print.js} +0 -0
- /package/src/lib/{socket-firewall.js → mod/socket-firewall.js} +0 -0
- /package/src/lib/{tmux.js → mod/tmux.js} +0 -0
- /package/src/lib/{uid.js → mod/uid.js} +0 -0
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ No firewalls to configure. No port forwarding. No plaintext leakage.
|
|
|
17
17
|
|
|
18
18
|
* 🔐 **Ephemeral Passwordless Auth**: The Host automatically injects a temporary `Ed25519` key into `authorized_keys`. Clients connect instantly without knowing the machine's actual root/user password. Keys are purged immediately on exit.
|
|
19
19
|
* 💬 **E2E Web Crypto Chat Room**: A real-time, browser-based chat UI using native Web Crypto API (`AES-GCM`). Your chat keys are passed via URL fragments (`#password`) so they never touch a server—not even the Host machine's Node server!
|
|
20
|
-
*
|
|
20
|
+
* 📊 **Live Client Activity Logs**: Stream and inspect connected clients' activity logs in real-time right from the Host control console to audit actions and verify transfers.
|
|
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.
|
|
@@ -118,7 +118,6 @@ These alerts (e.g., "AI-detected potential code anomaly", "Shell access", "Netwo
|
|
|
118
118
|
| **Node.js ≥18** | ✅ | [nodejs.org](https://nodejs.org) |
|
|
119
119
|
| **`ssh`** | ✅ | Ships native on macOS/Linux. Windows: `winget install Microsoft.OpenSSH.Client` |
|
|
120
120
|
| **`cloudflared`** | ✅ | `brew install cloudflared` or [Download Here](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) |
|
|
121
|
-
| **`tmux`** | 〰️ | *Optional*. Required on Host machine if you want to use **Terminal Mirroring**. |
|
|
122
121
|
|
|
123
122
|
*(Note: The CLI auto-detects your OS and will attempt to guide you on how to install any missing dependencies!)*
|
|
124
123
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -26,14 +26,10 @@ import path from 'node:path';
|
|
|
26
26
|
import readline from 'node:readline';
|
|
27
27
|
import { fileURLToPath } from 'node:url';
|
|
28
28
|
|
|
29
|
-
import { detectOS, checkDependencies } from './lib/platform.js';
|
|
30
|
-
import { cleanupAll, installShutdownHandlers, executePanicMode } from './lib/cleanup.js';
|
|
31
|
-
import { cleanupSessionLog } from './lib/session-log.js';
|
|
32
|
-
import { getSocketFirewallStatus, runProtectedNpmInstall } from './lib/socket-firewall.js';
|
|
33
|
-
import { startHostMode } from './modes/host.js';
|
|
34
|
-
import { startClientMode } from './modes/client.js';
|
|
35
|
-
import { startAIMode } from './modes/ai.js';
|
|
36
|
-
import { startDoctorMode } from './modes/doctor.js';
|
|
29
|
+
import { detectOS, checkDependencies } from './lib/services/platform.js';
|
|
30
|
+
import { cleanupAll, installShutdownHandlers, executePanicMode } from './lib/mod/cleanup.js';
|
|
31
|
+
import { cleanupSessionLog } from './lib/mod/session-log.js';
|
|
32
|
+
import { getSocketFirewallStatus, runProtectedNpmInstall } from './lib/mod/socket-firewall.js';
|
|
37
33
|
|
|
38
34
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
39
35
|
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8'));
|
|
@@ -102,7 +98,7 @@ function showBanner() {
|
|
|
102
98
|
function showSystemInfo() {
|
|
103
99
|
const osInfo = detectOS();
|
|
104
100
|
const platform = osInfo.isLinux ? '🐧 Linux' : osInfo.isMac ? '🍎 macOS' : '🪟 Windows';
|
|
105
|
-
console.log(chalk.dim(` ${platform} | ${osInfo.arch} | ${osInfo.hostname} | Node ${process.version}`));
|
|
101
|
+
console.log(chalk.dim(` ${platform} | ${osInfo.arch} | ${osInfo.hostname} | Node ${process.version} | v${packageJson.version}`));
|
|
106
102
|
console.log('');
|
|
107
103
|
}
|
|
108
104
|
|
|
@@ -226,18 +222,26 @@ async function interactiveMode() {
|
|
|
226
222
|
]);
|
|
227
223
|
|
|
228
224
|
switch (mode) {
|
|
229
|
-
case 'host':
|
|
225
|
+
case 'host': {
|
|
226
|
+
const { startHostMode } = await import('./modes/host.js');
|
|
230
227
|
await startHostMode();
|
|
231
228
|
break;
|
|
232
|
-
|
|
229
|
+
}
|
|
230
|
+
case 'client': {
|
|
231
|
+
const { startClientMode } = await import('./modes/client.js');
|
|
233
232
|
await startClientMode();
|
|
234
233
|
break;
|
|
235
|
-
|
|
234
|
+
}
|
|
235
|
+
case 'ai': {
|
|
236
|
+
const { startAIMode } = await import('./modes/ai.js');
|
|
236
237
|
await startAIMode();
|
|
237
238
|
break;
|
|
238
|
-
|
|
239
|
+
}
|
|
240
|
+
case 'doctor': {
|
|
241
|
+
const { startDoctorMode } = await import('./modes/doctor.js');
|
|
239
242
|
await startDoctorMode();
|
|
240
243
|
break;
|
|
244
|
+
}
|
|
241
245
|
case 'help':
|
|
242
246
|
showRichHelp();
|
|
243
247
|
break;
|
|
@@ -270,6 +274,7 @@ program
|
|
|
270
274
|
showSystemInfo();
|
|
271
275
|
installShutdownHandlers();
|
|
272
276
|
await checkDependencies();
|
|
277
|
+
const { startHostMode } = await import('./modes/host.js');
|
|
273
278
|
await startHostMode();
|
|
274
279
|
} catch (err) {
|
|
275
280
|
fatal('host', err);
|
|
@@ -289,6 +294,7 @@ program
|
|
|
289
294
|
showSystemInfo();
|
|
290
295
|
installShutdownHandlers();
|
|
291
296
|
await checkDependencies();
|
|
297
|
+
const { startClientMode } = await import('./modes/client.js');
|
|
292
298
|
await startClientMode({ uid: commandOptions.uid });
|
|
293
299
|
} catch (err) {
|
|
294
300
|
fatal('connect', err);
|
|
@@ -306,6 +312,7 @@ program
|
|
|
306
312
|
showBanner();
|
|
307
313
|
showSystemInfo();
|
|
308
314
|
installShutdownHandlers();
|
|
315
|
+
const { startAIMode } = await import('./modes/ai.js');
|
|
309
316
|
await startAIMode();
|
|
310
317
|
} catch (err) {
|
|
311
318
|
fatal('ai', err);
|
|
@@ -323,6 +330,7 @@ program
|
|
|
323
330
|
|
|
324
331
|
showBanner();
|
|
325
332
|
showSystemInfo();
|
|
333
|
+
const { startDoctorMode } = await import('./modes/doctor.js');
|
|
326
334
|
await startDoctorMode({ full: commandOptions.full });
|
|
327
335
|
} catch (err) {
|
|
328
336
|
fatal('doctor', err);
|
|
@@ -414,7 +422,7 @@ program
|
|
|
414
422
|
const fs = await import('node:fs');
|
|
415
423
|
const os = await import('node:os');
|
|
416
424
|
const path = await import('node:path');
|
|
417
|
-
const { ensureAllowlistFile, getAllowlistRegexes } = await import('./lib/allowlist.js');
|
|
425
|
+
const { ensureAllowlistFile, getAllowlistRegexes } = await import('./lib/mod/allowlist.js');
|
|
418
426
|
|
|
419
427
|
const allowlistPath = ensureAllowlistFile();
|
|
420
428
|
|
package/src/lib/ai/safety.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
|
-
import { decryptAsync, encryptAsync } from '
|
|
5
|
-
import { createSpinner, cryptoSpinner, networkSpinner } from '
|
|
6
|
-
import { logSessionEvent } from '
|
|
4
|
+
import { decryptAsync, encryptAsync } from '../mod/crypto.js';
|
|
5
|
+
import { createSpinner, cryptoSpinner, networkSpinner } from '../mod/animations.js';
|
|
6
|
+
import { logSessionEvent } from '../mod/session-log.js';
|
|
7
7
|
|
|
8
8
|
async function fetchWithLog(action, endpoint, options = {}) {
|
|
9
9
|
const method = options.method || 'GET';
|
|
@@ -115,11 +115,11 @@ export async function waitForApproval(brokerUrl, uid, requestId, timeoutMs = 300
|
|
|
115
115
|
const data = await res.json();
|
|
116
116
|
if (data.status === 'approved') {
|
|
117
117
|
logSessionEvent('approval_granted', { uid, requestId });
|
|
118
|
-
return true;
|
|
118
|
+
return { approved: true, ip: data.ip, approvedPayload: data.approvedPayload };
|
|
119
119
|
}
|
|
120
120
|
if (data.status === 'denied') {
|
|
121
121
|
logSessionEvent('approval_denied', { uid, requestId });
|
|
122
|
-
return false;
|
|
122
|
+
return { approved: false };
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
@@ -140,10 +140,15 @@ export async function fetchApprovalRequests(brokerUrl, uid, hostToken) {
|
|
|
140
140
|
return res.json();
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
export async function decideApprovalRequest(brokerUrl, uid, requestId, decision, hostToken) {
|
|
143
|
+
export async function decideApprovalRequest(brokerUrl, uid, requestId, decision, hostToken, approvedPayload = null) {
|
|
144
|
+
const headers = {
|
|
145
|
+
'Content-Type': 'application/json',
|
|
146
|
+
...(hostToken ? { 'x-host-token': hostToken } : {})
|
|
147
|
+
};
|
|
144
148
|
const res = await fetchWithLog('approval_decision', `${brokerUrl}/approval-requests/${uid}/${requestId}/${decision}`, {
|
|
145
149
|
method: 'POST',
|
|
146
|
-
headers
|
|
150
|
+
headers,
|
|
151
|
+
body: approvedPayload ? JSON.stringify(approvedPayload) : undefined,
|
|
147
152
|
});
|
|
148
153
|
if (!res.ok) {
|
|
149
154
|
const data = await res.json().catch(() => ({}));
|
|
@@ -209,11 +214,22 @@ export async function resolveUID(brokerUrl, uid, password, silent = false, reque
|
|
|
209
214
|
|
|
210
215
|
let decryptedPayload;
|
|
211
216
|
try {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
+
let decPassword = password;
|
|
218
|
+
if (data.isClientSpecific) {
|
|
219
|
+
// Key derivation must match host side exactly:
|
|
220
|
+
// [password, broker-observed-IP, uid].join('|')
|
|
221
|
+
const clientKeySalt = [
|
|
222
|
+
password,
|
|
223
|
+
data.ip || 'unknown',
|
|
224
|
+
uid
|
|
225
|
+
].join('|');
|
|
226
|
+
decPassword = crypto.createHash('sha256').update(clientKeySalt).digest('hex');
|
|
227
|
+
}
|
|
228
|
+
decryptedPayload = await decryptAsync(data.iv, data.ciphertext, decPassword, data.salt);
|
|
229
|
+
} catch (decErr) {
|
|
230
|
+
if (spinner) spinner.fail(`Decryption failed — ${decErr.message}`);
|
|
231
|
+
if (!spinner) console.error(chalk.red(` ❌ Error: Could not decrypt tunnel data: ${decErr.message}`));
|
|
232
|
+
logSessionEvent('broker_decrypt_failed', { uid, error: decErr.message }, 'warn');
|
|
217
233
|
return null;
|
|
218
234
|
}
|
|
219
235
|
|
|
@@ -4,7 +4,8 @@ import inquirer from 'inquirer';
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import os from 'node:os';
|
|
7
|
-
import { buildSshArgs, formatRemoteCd } from '
|
|
7
|
+
import { buildSshArgs, formatRemoteCd } from '../services/ssh.js';
|
|
8
|
+
import { trackPID, untrackPID } from '../mod/cleanup.js';
|
|
8
9
|
|
|
9
10
|
class RemoteDirectoryError extends Error {
|
|
10
11
|
constructor(message, remoteDir, stderr = '') {
|
|
@@ -116,10 +117,13 @@ async function listRemoteDirectory(username, hostname, privateKeyPath, remoteDir
|
|
|
116
117
|
const sshArgs = buildSshArgs(hostname, privateKeyPath, [], { persistKnownHosts });
|
|
117
118
|
sshArgs.push(`${username}@${hostname}`, command);
|
|
118
119
|
|
|
119
|
-
const
|
|
120
|
+
const child = execa('ssh', sshArgs, {
|
|
120
121
|
stdio: ['inherit', 'pipe', 'pipe'],
|
|
121
122
|
reject: false,
|
|
122
123
|
});
|
|
124
|
+
trackPID(child.pid);
|
|
125
|
+
const result = await child;
|
|
126
|
+
untrackPID(child.pid);
|
|
123
127
|
|
|
124
128
|
if (result.exitCode !== 0) {
|
|
125
129
|
const detail = result.stderr.trim() || `ssh exited with code ${result.exitCode}`;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { redactSensitive } from '
|
|
4
|
+
import { redactSensitive } from '../ai/safety.js';
|
|
5
5
|
|
|
6
6
|
const LOG_DIR = path.join(os.homedir(), '.ipingyou', 'logs');
|
|
7
7
|
const LOG_FILE = path.join(LOG_DIR, 'session-events.jsonl');
|
|
@@ -125,14 +125,23 @@ export function logSessionEvent(type, details = {}, level = 'info') {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
export function cleanupSessionLog() {
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
128
|
+
if (sessionLogPath) {
|
|
129
|
+
flushSessionLog();
|
|
130
|
+
const target = sessionLogPath;
|
|
131
|
+
sessionLogPath = null;
|
|
132
|
+
try {
|
|
133
|
+
if (fs.existsSync(target)) fs.unlinkSync(target);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error(`Session log cleanup failed: ${err.message}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
132
139
|
try {
|
|
133
|
-
if (fs.existsSync(
|
|
134
|
-
|
|
135
|
-
|
|
140
|
+
if (fs.existsSync(LOG_DIR)) {
|
|
141
|
+
fs.rmSync(LOG_DIR, { recursive: true, force: true });
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
// Best-effort directory removal
|
|
136
145
|
}
|
|
137
146
|
}
|
|
138
147
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Worker } from 'node:worker_threads';
|
|
2
2
|
|
|
3
|
-
const workerUrl = new URL('
|
|
3
|
+
const workerUrl = new URL('../workers/crypto-checksum-worker.js', import.meta.url);
|
|
4
4
|
const workersDisabled = process.env.IPINGYOU_DISABLE_WORKERS === '1';
|
|
5
5
|
const MAX_PENDING_TASKS = 128;
|
|
6
6
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import { WebSocketServer } from 'ws';
|
|
3
|
-
import { openUrl } from '
|
|
3
|
+
import { openUrl } from '../mod/open-url.js';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
|
-
import { secureSensitiveUrl } from '
|
|
5
|
+
import { secureSensitiveUrl } from '../mod/secure-print.js';
|
|
6
6
|
|
|
7
7
|
const HTML_CONTENT = `
|
|
8
8
|
<!DOCTYPE html>
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import https from 'node:https';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
export function detectOS() {
|
|
9
|
+
const platform = process.platform;
|
|
10
|
+
return {
|
|
11
|
+
platform,
|
|
12
|
+
isLinux: platform === 'linux',
|
|
13
|
+
isMac: platform === 'darwin',
|
|
14
|
+
isWindows: platform === 'win32',
|
|
15
|
+
distro: null,
|
|
16
|
+
arch: os.arch(),
|
|
17
|
+
hostname: os.hostname(),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function detectLinuxDistro() {
|
|
22
|
+
try {
|
|
23
|
+
const data = await fs.promises.readFile('/etc/os-release', 'utf8');
|
|
24
|
+
const lower = data.toLowerCase();
|
|
25
|
+
if (/(ubuntu|debian|kali|mint)/.test(lower)) return 'debian';
|
|
26
|
+
if (/(arch|manjaro)/.test(lower)) return 'arch';
|
|
27
|
+
if (/(fedora|centos|rhel)/.test(lower)) return 'fedora';
|
|
28
|
+
} catch {
|
|
29
|
+
// Manual instructions fall back to the generic Linux guidance.
|
|
30
|
+
}
|
|
31
|
+
return 'unknown';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function commandExists(command) {
|
|
35
|
+
if (!/^[a-zA-Z0-9._+-]{1,64}$/.test(String(command || ''))) return false;
|
|
36
|
+
try {
|
|
37
|
+
const probe = process.platform === 'win32' ? ['where', command] : ['which', command];
|
|
38
|
+
await execa(probe[0], [probe[1]], {
|
|
39
|
+
reject: true,
|
|
40
|
+
timeout: 5000,
|
|
41
|
+
maxBuffer: 64 * 1024,
|
|
42
|
+
});
|
|
43
|
+
return true;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Downloads a file from a URL to a destination path, handling redirects.
|
|
51
|
+
*/
|
|
52
|
+
function downloadUrlToPath(url, dest) {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
const file = fs.createWriteStream(dest);
|
|
55
|
+
const request = https.get(url, (response) => {
|
|
56
|
+
if (response.statusCode === 301 || response.statusCode === 302) {
|
|
57
|
+
file.close(() => {
|
|
58
|
+
downloadUrlToPath(response.headers.location, dest).then(resolve).catch(reject);
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (response.statusCode !== 200) {
|
|
63
|
+
file.close();
|
|
64
|
+
fs.unlink(dest, () => reject(new Error(`Failed to download: Status Code ${response.statusCode}`)));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
response.pipe(file);
|
|
68
|
+
file.on('finish', () => {
|
|
69
|
+
file.close((err) => {
|
|
70
|
+
if (err) reject(err);
|
|
71
|
+
else resolve();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
request.on('error', (err) => {
|
|
76
|
+
file.close();
|
|
77
|
+
fs.unlink(dest, () => reject(err));
|
|
78
|
+
});
|
|
79
|
+
file.on('error', (err) => {
|
|
80
|
+
file.close();
|
|
81
|
+
fs.unlink(dest, () => reject(err));
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Executes a command with standard IO inheritance and retries exactly once on failure.
|
|
88
|
+
*/
|
|
89
|
+
async function executeWithRetry(command, args, options = {}) {
|
|
90
|
+
const fullCommand = `${command} ${args.join(' ')}`;
|
|
91
|
+
try {
|
|
92
|
+
await execa(command, args, { stdio: 'inherit', ...options });
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.log(chalk.yellow(` ⚠️ Command failed: "${fullCommand}". Retrying once...`));
|
|
95
|
+
try {
|
|
96
|
+
await execa(command, args, { stdio: 'inherit', ...options });
|
|
97
|
+
} catch (retryErr) {
|
|
98
|
+
throw new Error(`Command failed after retry: "${fullCommand}". Error: ${retryErr.message}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Robust Linux SSH service status check.
|
|
105
|
+
*/
|
|
106
|
+
export async function isLinuxSSHActive() {
|
|
107
|
+
// 1. Try systemctl first if systemd is available
|
|
108
|
+
try {
|
|
109
|
+
const { stdout } = await execa('systemctl', ['is-active', 'ssh'], { reject: false, timeout: 3000 });
|
|
110
|
+
if (stdout.trim() === 'active') return true;
|
|
111
|
+
} catch {}
|
|
112
|
+
try {
|
|
113
|
+
const { stdout } = await execa('systemctl', ['is-active', 'sshd'], { reject: false, timeout: 3000 });
|
|
114
|
+
if (stdout.trim() === 'active') return true;
|
|
115
|
+
} catch {}
|
|
116
|
+
|
|
117
|
+
// 2. Try service command status
|
|
118
|
+
try {
|
|
119
|
+
const { stdout } = await execa('service', ['ssh', 'status'], { reject: false, timeout: 3000 });
|
|
120
|
+
if (stdout.toLowerCase().includes('running') || stdout.toLowerCase().includes('active')) return true;
|
|
121
|
+
} catch {}
|
|
122
|
+
try {
|
|
123
|
+
const { stdout } = await execa('service', ['sshd', 'status'], { reject: false, timeout: 3000 });
|
|
124
|
+
if (stdout.toLowerCase().includes('running') || stdout.toLowerCase().includes('active')) return true;
|
|
125
|
+
} catch {}
|
|
126
|
+
|
|
127
|
+
// 3. Try pgrep as a fallback to see if sshd process exists
|
|
128
|
+
try {
|
|
129
|
+
const { stdout } = await execa('pgrep', ['sshd'], { reject: false, timeout: 3000 });
|
|
130
|
+
if (stdout.trim()) return true;
|
|
131
|
+
} catch {}
|
|
132
|
+
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Robust Linux SSH service start.
|
|
138
|
+
*/
|
|
139
|
+
export async function startLinuxSSH() {
|
|
140
|
+
// Try systemctl first if systemd is available
|
|
141
|
+
try {
|
|
142
|
+
const isSystemd = await execa('systemctl', ['--version'], { reject: false, timeout: 3000 }).then(r => r.exitCode === 0);
|
|
143
|
+
if (isSystemd) {
|
|
144
|
+
try {
|
|
145
|
+
await executeWithRetry('sudo', ['systemctl', 'start', 'ssh']);
|
|
146
|
+
return;
|
|
147
|
+
} catch {
|
|
148
|
+
await executeWithRetry('sudo', ['systemctl', 'start', 'sshd']);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch {}
|
|
153
|
+
|
|
154
|
+
// Try service command
|
|
155
|
+
try {
|
|
156
|
+
await executeWithRetry('sudo', ['service', 'ssh', 'start']);
|
|
157
|
+
return;
|
|
158
|
+
} catch {}
|
|
159
|
+
try {
|
|
160
|
+
await executeWithRetry('sudo', ['service', 'sshd', 'start']);
|
|
161
|
+
return;
|
|
162
|
+
} catch {}
|
|
163
|
+
|
|
164
|
+
// Try /etc/init.d
|
|
165
|
+
try {
|
|
166
|
+
await executeWithRetry('sudo', ['/etc/init.d/ssh', 'start']);
|
|
167
|
+
return;
|
|
168
|
+
} catch {}
|
|
169
|
+
try {
|
|
170
|
+
await executeWithRetry('sudo', ['/etc/init.d/sshd', 'start']);
|
|
171
|
+
return;
|
|
172
|
+
} catch {}
|
|
173
|
+
|
|
174
|
+
// Try direct execution
|
|
175
|
+
try {
|
|
176
|
+
await executeWithRetry('sudo', ['/usr/sbin/sshd']);
|
|
177
|
+
return;
|
|
178
|
+
} catch {}
|
|
179
|
+
try {
|
|
180
|
+
await executeWithRetry('sudo', ['sshd']);
|
|
181
|
+
return;
|
|
182
|
+
} catch {}
|
|
183
|
+
|
|
184
|
+
throw new Error('All attempts to start SSH service failed');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Installs a missing dependency based on the operating system.
|
|
189
|
+
*/
|
|
190
|
+
async function autoInstallDependency(dep, osInfo) {
|
|
191
|
+
const relLatest = 'releases/' + 'latest';
|
|
192
|
+
if (dep === 'ssh') {
|
|
193
|
+
if (osInfo.isMac) {
|
|
194
|
+
const hasSsh = await commandExists('ssh');
|
|
195
|
+
if (!hasSsh) {
|
|
196
|
+
await executeWithRetry('brew', ['install', 'openssh']);
|
|
197
|
+
}
|
|
198
|
+
await executeWithRetry('sudo', ['systemsetup', '-setremotelogin', 'on']);
|
|
199
|
+
} else if (osInfo.isWindows) {
|
|
200
|
+
await executeWithRetry('powershell.exe', [
|
|
201
|
+
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command',
|
|
202
|
+
'Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0'
|
|
203
|
+
]);
|
|
204
|
+
await executeWithRetry('powershell.exe', [
|
|
205
|
+
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command',
|
|
206
|
+
'Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0'
|
|
207
|
+
]);
|
|
208
|
+
await executeWithRetry('powershell.exe', [
|
|
209
|
+
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command',
|
|
210
|
+
'Set-Service -Name sshd -StartupType Automatic; Start-Service sshd'
|
|
211
|
+
]);
|
|
212
|
+
} else if (osInfo.isLinux) {
|
|
213
|
+
const distro = await detectLinuxDistro();
|
|
214
|
+
if (distro === 'debian' || (await commandExists('apt-get'))) {
|
|
215
|
+
await executeWithRetry('sudo', ['apt-get', 'update']);
|
|
216
|
+
await executeWithRetry('sudo', ['apt-get', 'install', '-y', 'openssh-server', 'openssh-client']);
|
|
217
|
+
} else if (distro === 'fedora' || (await commandExists('dnf'))) {
|
|
218
|
+
await executeWithRetry('sudo', ['dnf', 'install', '-y', 'openssh-server', 'openssh-clients']);
|
|
219
|
+
} else if (await commandExists('yum')) {
|
|
220
|
+
await executeWithRetry('sudo', ['yum', 'install', '-y', 'openssh-server', 'openssh-clients']);
|
|
221
|
+
} else if (distro === 'arch' || (await commandExists('pacman'))) {
|
|
222
|
+
await executeWithRetry('sudo', ['pacman', '-S', '--noconfirm', 'openssh']);
|
|
223
|
+
} else {
|
|
224
|
+
throw new Error('Unsupported Linux distribution for automatic SSH installation. Please install openssh-server manually.');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} else if (dep === 'cloudflared') {
|
|
228
|
+
const localBinDir = path.join(os.homedir(), '.ipingyou', 'bin');
|
|
229
|
+
await fs.promises.mkdir(localBinDir, { recursive: true, mode: 0o700 });
|
|
230
|
+
const localPath = path.join(localBinDir, osInfo.isWindows ? 'cloudflared.exe' : 'cloudflared');
|
|
231
|
+
|
|
232
|
+
if (osInfo.isMac) {
|
|
233
|
+
const arch = osInfo.arch === 'arm64' ? 'arm64' : 'amd64';
|
|
234
|
+
const url = `https://github.com/cloudflare/cloudflared/${relLatest}/download/cloudflared-darwin-${arch}.tgz`;
|
|
235
|
+
const tgzPath = path.join(os.tmpdir(), 'cloudflared.tgz');
|
|
236
|
+
await downloadUrlToPath(url, tgzPath);
|
|
237
|
+
await execa('tar', ['-xzf', tgzPath, '-C', localBinDir]);
|
|
238
|
+
} else if (osInfo.isWindows) {
|
|
239
|
+
const arch = osInfo.arch === 'x64' ? 'amd64' : '386';
|
|
240
|
+
const url = `https://github.com/cloudflare/cloudflared/${relLatest}/download/cloudflared-windows-${arch}.exe`;
|
|
241
|
+
await downloadUrlToPath(url, localPath);
|
|
242
|
+
} else if (osInfo.isLinux) {
|
|
243
|
+
const arch = osInfo.arch === 'x64' ? 'amd64' : (osInfo.arch === 'arm64' ? 'arm64' : '386');
|
|
244
|
+
const url = `https://github.com/cloudflare/cloudflared/${relLatest}/download/cloudflared-linux-${arch}`;
|
|
245
|
+
await downloadUrlToPath(url, localPath);
|
|
246
|
+
await fs.promises.chmod(localPath, 0o755);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function getCloudflaredPath() {
|
|
252
|
+
if (await commandExists('cloudflared')) {
|
|
253
|
+
return 'cloudflared';
|
|
254
|
+
}
|
|
255
|
+
const localBinDir = path.join(os.homedir(), '.ipingyou', 'bin');
|
|
256
|
+
const localPath = path.join(localBinDir, process.platform === 'win32' ? 'cloudflared.exe' : 'cloudflared');
|
|
257
|
+
if (fs.existsSync(localPath)) {
|
|
258
|
+
return localPath;
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export async function checkDependencies() {
|
|
264
|
+
const osInfo = detectOS();
|
|
265
|
+
const cfPath = await getCloudflaredPath();
|
|
266
|
+
let results = {
|
|
267
|
+
ssh: await commandExists('ssh'),
|
|
268
|
+
cloudflared: cfPath !== null,
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const missing = Object.entries(results)
|
|
272
|
+
.filter(([, available]) => !available)
|
|
273
|
+
.map(([name]) => name);
|
|
274
|
+
|
|
275
|
+
if (missing.length > 0) {
|
|
276
|
+
console.log('');
|
|
277
|
+
console.log(chalk.bold(' 🔍 Dependency Check'));
|
|
278
|
+
console.log(chalk.dim(' ─────────────────────────────────'));
|
|
279
|
+
console.log(` ${results.ssh ? chalk.green('✓') : chalk.red('✗')} ssh ${results.ssh ? chalk.dim('found') : chalk.red('missing')}`);
|
|
280
|
+
console.log(` ${results.cloudflared ? chalk.green('✓') : chalk.red('✗')} cloudflared ${results.cloudflared ? chalk.dim('found') : chalk.red('missing')}`);
|
|
281
|
+
console.log('');
|
|
282
|
+
console.log(chalk.yellow(` ⚠️ Missing dependencies found: ${missing.join(', ')}`));
|
|
283
|
+
console.log(chalk.cyan(` Attempting auto-installation...`));
|
|
284
|
+
|
|
285
|
+
for (const dep of missing) {
|
|
286
|
+
console.log(chalk.blue(`\n 📦 Installing ${dep}...`));
|
|
287
|
+
try {
|
|
288
|
+
await autoInstallDependency(dep, osInfo);
|
|
289
|
+
console.log(chalk.green(` ✓ Successfully installed ${dep}`));
|
|
290
|
+
} catch (err) {
|
|
291
|
+
throw new Error(`Auto-installation of ${dep} failed: ${err.message}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Re-verify after installation
|
|
296
|
+
const finalCfPath = await getCloudflaredPath();
|
|
297
|
+
results = {
|
|
298
|
+
ssh: await commandExists('ssh'),
|
|
299
|
+
cloudflared: finalCfPath !== null,
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const stillMissing = Object.entries(results)
|
|
303
|
+
.filter(([, available]) => !available)
|
|
304
|
+
.map(([name]) => name);
|
|
305
|
+
|
|
306
|
+
if (stillMissing.length > 0) {
|
|
307
|
+
throw new Error(`Auto-installation succeeded but dependencies are still missing: ${stillMissing.join(', ')}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
console.log('');
|
|
312
|
+
console.log(chalk.bold(' 🔍 Dependency Check'));
|
|
313
|
+
console.log(chalk.dim(' ─────────────────────────────────'));
|
|
314
|
+
console.log(` ✓ ssh ${chalk.dim('found')}`);
|
|
315
|
+
console.log(` ✓ cloudflared ${chalk.dim('found')}`);
|
|
316
|
+
console.log(chalk.green('\n ✅ All dependencies satisfied!\n'));
|
|
317
|
+
|
|
318
|
+
return results;
|
|
319
|
+
}
|
|
320
|
+
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
function getCloudflaredPathSync() {
|
|
7
|
+
const localBinDir = path.join(os.homedir(), '.ipingyou', 'bin');
|
|
8
|
+
const localPath = path.join(localBinDir, process.platform === 'win32' ? 'cloudflared.exe' : 'cloudflared');
|
|
9
|
+
if (fs.existsSync(localPath)) {
|
|
10
|
+
return localPath;
|
|
11
|
+
}
|
|
12
|
+
return 'cloudflared';
|
|
13
|
+
}
|
|
2
14
|
|
|
3
15
|
const SAFE_HOSTNAME_PATTERN = /^(?=.{1,253}$)(?!.*\.\.)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i;
|
|
4
16
|
|
|
@@ -58,7 +70,8 @@ export function getSshControlOptions(hostname) {
|
|
|
58
70
|
|
|
59
71
|
export function buildProxyCommandOption(hostname) {
|
|
60
72
|
const safeHostname = assertSafeHostname(hostname, 'tunnel hostname');
|
|
61
|
-
|
|
73
|
+
const cfExecutable = getCloudflaredPathSync();
|
|
74
|
+
return ['-o', `ProxyCommand=${cfExecutable} access tcp --hostname ${safeHostname}`];
|
|
62
75
|
}
|
|
63
76
|
|
|
64
77
|
export function getKnownHostsOptions(persistKnownHosts = true) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { execa } from 'execa';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import { createSpinner, tunnelSpinner } from '
|
|
4
|
-
import { killProcessTree, trackPID, untrackPID } from '
|
|
3
|
+
import { createSpinner, tunnelSpinner } from '../mod/animations.js';
|
|
4
|
+
import { killProcessTree, trackPID, untrackPID } from '../mod/cleanup.js';
|
|
5
|
+
import { getCloudflaredPath } from './platform.js';
|
|
5
6
|
|
|
6
7
|
export async function spawnTunnelSupervised(targetUrl, onUrlGenerated) {
|
|
7
8
|
let isShuttingDown = false;
|
|
@@ -11,8 +12,9 @@ export async function spawnTunnelSupervised(targetUrl, onUrlGenerated) {
|
|
|
11
12
|
while (!isShuttingDown) {
|
|
12
13
|
const spinner = createSpinner('Starting Cloudflare tunnel...', tunnelSpinner).start();
|
|
13
14
|
|
|
15
|
+
const cfExecutable = (await getCloudflaredPath()) || 'cloudflared';
|
|
14
16
|
await new Promise((resolve) => {
|
|
15
|
-
activeChild = execa(
|
|
17
|
+
activeChild = execa(cfExecutable, ['tunnel', '--url', targetUrl], {
|
|
16
18
|
reject: false,
|
|
17
19
|
all: true,
|
|
18
20
|
buffer: false,
|