@miraj181/ipingyou 2.1.19 → 2.1.22
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} +24 -8
- 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 +364 -0
- package/src/lib/{tunnel.js → services/tunnel.js} +2 -2
- package/src/modes/ai.js +18 -9
- package/src/modes/client.js +90 -25
- package/src/modes/doctor.js +5 -7
- package/src/modes/host.js +303 -153
- 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/src/lib/{ssh.js → services/ssh.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,7 +214,18 @@ export async function resolveUID(brokerUrl, uid, password, silent = false, reque
|
|
|
209
214
|
|
|
210
215
|
let decryptedPayload;
|
|
211
216
|
try {
|
|
212
|
-
|
|
217
|
+
let decPassword = password;
|
|
218
|
+
if (data.isClientSpecific) {
|
|
219
|
+
const clientKeySalt = [
|
|
220
|
+
password,
|
|
221
|
+
data.ip || 'unknown',
|
|
222
|
+
os.userInfo().username,
|
|
223
|
+
os.hostname(),
|
|
224
|
+
`${os.type()} ${os.release()} (${os.arch()})`
|
|
225
|
+
].join('|');
|
|
226
|
+
decPassword = crypto.createHash('sha256').update(clientKeySalt).digest('hex');
|
|
227
|
+
}
|
|
228
|
+
decryptedPayload = await decryptAsync(data.iv, data.ciphertext, decPassword, data.salt);
|
|
213
229
|
} catch {
|
|
214
230
|
if (spinner) spinner.fail('Decryption failed — incorrect password or corrupted data');
|
|
215
231
|
if (!spinner) console.error(chalk.red(' ❌ Error: Could not decrypt tunnel data. Incorrect password.'));
|
|
@@ -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,364 @@
|
|
|
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
|
+
if (osInfo.isMac) {
|
|
229
|
+
const hasBrew = await commandExists('brew');
|
|
230
|
+
if (hasBrew) {
|
|
231
|
+
await executeWithRetry('brew', ['install', 'cloudflared']);
|
|
232
|
+
} else {
|
|
233
|
+
console.log(chalk.yellow(' Homebrew not found. Downloading cloudflared binary directly...'));
|
|
234
|
+
const arch = osInfo.arch === 'arm64' ? 'arm64' : 'amd64';
|
|
235
|
+
const url = `https://github.com/cloudflare/cloudflared/${relLatest}/download/cloudflared-darwin-${arch}.tgz`;
|
|
236
|
+
const tgzPath = path.join(os.tmpdir(), 'cloudflared.tgz');
|
|
237
|
+
await downloadUrlToPath(url, tgzPath);
|
|
238
|
+
await executeWithRetry('tar', ['-xzf', tgzPath, '-C', os.tmpdir()]);
|
|
239
|
+
await executeWithRetry('sudo', ['cp', path.join(os.tmpdir(), 'cloudflared'), '/usr/local/bin/cloudflared']);
|
|
240
|
+
await executeWithRetry('sudo', ['chmod', '+x', '/usr/local/bin/cloudflared']);
|
|
241
|
+
}
|
|
242
|
+
} else if (osInfo.isWindows) {
|
|
243
|
+
const hasWinget = await commandExists('winget');
|
|
244
|
+
if (hasWinget) {
|
|
245
|
+
await executeWithRetry('winget', ['install', '--id', 'Cloudflare.cloudflared', '--silent', '--accept-source-agreements', '--accept-package-agreements']);
|
|
246
|
+
} else {
|
|
247
|
+
console.log(chalk.yellow(' winget not found. Downloading cloudflared executable directly...'));
|
|
248
|
+
const arch = osInfo.arch === 'x64' ? 'amd64' : '386';
|
|
249
|
+
const url = `https://github.com/cloudflare/cloudflared/${relLatest}/download/cloudflared-windows-${arch}.exe`;
|
|
250
|
+
const destDir = path.join(process.env.LOCALAPPDATA, 'Microsoft', 'WindowsApps');
|
|
251
|
+
await fs.promises.mkdir(destDir, { recursive: true });
|
|
252
|
+
const destPath = path.join(destDir, 'cloudflared.exe');
|
|
253
|
+
await downloadUrlToPath(url, destPath);
|
|
254
|
+
console.log(chalk.green(` Downloaded cloudflared to ${destPath}`));
|
|
255
|
+
}
|
|
256
|
+
} else if (osInfo.isLinux) {
|
|
257
|
+
const distro = await detectLinuxDistro();
|
|
258
|
+
const arch = osInfo.arch === 'x64' ? 'amd64' : (osInfo.arch === 'arm64' ? 'arm64' : '386');
|
|
259
|
+
|
|
260
|
+
let installed = false;
|
|
261
|
+
if (distro === 'debian' || (await commandExists('apt-get'))) {
|
|
262
|
+
try {
|
|
263
|
+
const url = `https://github.com/cloudflare/cloudflared/${relLatest}/download/cloudflared-linux-${arch}.deb`;
|
|
264
|
+
const debPath = path.join(os.tmpdir(), 'cloudflared.deb');
|
|
265
|
+
await downloadUrlToPath(url, debPath);
|
|
266
|
+
try {
|
|
267
|
+
await executeWithRetry('sudo', ['dpkg', '-i', debPath]);
|
|
268
|
+
} catch {
|
|
269
|
+
await executeWithRetry('sudo', ['apt-get', 'update']);
|
|
270
|
+
await executeWithRetry('sudo', ['apt-get', 'install', '-f', '-y']);
|
|
271
|
+
}
|
|
272
|
+
installed = true;
|
|
273
|
+
} catch (err) {
|
|
274
|
+
console.log(chalk.yellow(` Failed to install via deb package: ${err.message}. Trying direct binary...`));
|
|
275
|
+
}
|
|
276
|
+
} else if (distro === 'fedora' || (await commandExists('dnf')) || (await commandExists('yum'))) {
|
|
277
|
+
try {
|
|
278
|
+
const rpmArch = osInfo.arch === 'x64' ? 'x86_64' : (osInfo.arch === 'arm64' ? 'aarch64' : 'i386');
|
|
279
|
+
const url = `https://github.com/cloudflare/cloudflared/${relLatest}/download/cloudflared-linux-${rpmArch}.rpm`;
|
|
280
|
+
const rpmPath = path.join(os.tmpdir(), 'cloudflared.rpm');
|
|
281
|
+
await downloadUrlToPath(url, rpmPath);
|
|
282
|
+
const pkgManager = await commandExists('dnf') ? 'dnf' : 'yum';
|
|
283
|
+
await executeWithRetry('sudo', [pkgManager, 'install', '-y', rpmPath]);
|
|
284
|
+
installed = true;
|
|
285
|
+
} catch (err) {
|
|
286
|
+
console.log(chalk.yellow(` Failed to install via rpm package: ${err.message}. Trying direct binary...`));
|
|
287
|
+
}
|
|
288
|
+
} else if (distro === 'arch' || (await commandExists('pacman'))) {
|
|
289
|
+
try {
|
|
290
|
+
await executeWithRetry('sudo', ['pacman', '-S', '--noconfirm', 'cloudflared']);
|
|
291
|
+
installed = true;
|
|
292
|
+
} catch (err) {
|
|
293
|
+
console.log(chalk.yellow(` Failed to install via pacman: ${err.message}. Trying direct binary...`));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (!installed) {
|
|
298
|
+
console.log(chalk.yellow(' Downloading cloudflared binary directly...'));
|
|
299
|
+
const url = `https://github.com/cloudflare/cloudflared/${relLatest}/download/cloudflared-linux-${arch}`;
|
|
300
|
+
const binPath = path.join(os.tmpdir(), 'cloudflared');
|
|
301
|
+
await downloadUrlToPath(url, binPath);
|
|
302
|
+
await executeWithRetry('sudo', ['cp', binPath, '/usr/local/bin/cloudflared']);
|
|
303
|
+
await executeWithRetry('sudo', ['chmod', '+x', '/usr/local/bin/cloudflared']);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export async function checkDependencies() {
|
|
310
|
+
const osInfo = detectOS();
|
|
311
|
+
let results = {
|
|
312
|
+
ssh: await commandExists('ssh'),
|
|
313
|
+
cloudflared: await commandExists('cloudflared'),
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const missing = Object.entries(results)
|
|
317
|
+
.filter(([, available]) => !available)
|
|
318
|
+
.map(([name]) => name);
|
|
319
|
+
|
|
320
|
+
if (missing.length > 0) {
|
|
321
|
+
console.log('');
|
|
322
|
+
console.log(chalk.bold(' 🔍 Dependency Check'));
|
|
323
|
+
console.log(chalk.dim(' ─────────────────────────────────'));
|
|
324
|
+
console.log(` ${results.ssh ? chalk.green('✓') : chalk.red('✗')} ssh ${results.ssh ? chalk.dim('found') : chalk.red('missing')}`);
|
|
325
|
+
console.log(` ${results.cloudflared ? chalk.green('✓') : chalk.red('✗')} cloudflared ${results.cloudflared ? chalk.dim('found') : chalk.red('missing')}`);
|
|
326
|
+
console.log('');
|
|
327
|
+
console.log(chalk.yellow(` ⚠️ Missing dependencies found: ${missing.join(', ')}`));
|
|
328
|
+
console.log(chalk.cyan(` Attempting auto-installation...`));
|
|
329
|
+
|
|
330
|
+
for (const dep of missing) {
|
|
331
|
+
console.log(chalk.blue(`\n 📦 Installing ${dep}...`));
|
|
332
|
+
try {
|
|
333
|
+
await autoInstallDependency(dep, osInfo);
|
|
334
|
+
console.log(chalk.green(` ✓ Successfully installed ${dep}`));
|
|
335
|
+
} catch (err) {
|
|
336
|
+
throw new Error(`Auto-installation of ${dep} failed: ${err.message}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Re-verify after installation
|
|
341
|
+
results = {
|
|
342
|
+
ssh: await commandExists('ssh'),
|
|
343
|
+
cloudflared: await commandExists('cloudflared'),
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const stillMissing = Object.entries(results)
|
|
347
|
+
.filter(([, available]) => !available)
|
|
348
|
+
.map(([name]) => name);
|
|
349
|
+
|
|
350
|
+
if (stillMissing.length > 0) {
|
|
351
|
+
throw new Error(`Auto-installation succeeded but dependencies are still missing: ${stillMissing.join(', ')}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
console.log('');
|
|
356
|
+
console.log(chalk.bold(' 🔍 Dependency Check'));
|
|
357
|
+
console.log(chalk.dim(' ─────────────────────────────────'));
|
|
358
|
+
console.log(` ✓ ssh ${chalk.dim('found')}`);
|
|
359
|
+
console.log(` ✓ cloudflared ${chalk.dim('found')}`);
|
|
360
|
+
console.log(chalk.green('\n ✅ All dependencies satisfied!\n'));
|
|
361
|
+
|
|
362
|
+
return results;
|
|
363
|
+
}
|
|
364
|
+
|
|
@@ -1,7 +1,7 @@
|
|
|
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
5
|
|
|
6
6
|
export async function spawnTunnelSupervised(targetUrl, onUrlGenerated) {
|
|
7
7
|
let isShuttingDown = false;
|