@miraj181/ipingyou 2.1.6 → 2.1.15
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/package.json +8 -7
- package/src/cli.js +31 -28
- package/src/lib/broker.js +18 -9
- package/src/lib/chat.js +9 -2
- package/src/lib/checksum.js +22 -2
- package/src/lib/cleanup.js +18 -8
- package/src/lib/crypto.js +27 -0
- package/src/lib/platform.js +33 -10
- package/src/lib/secure-print.js +86 -0
- package/src/lib/session-log.js +78 -3
- package/src/lib/ssh.js +25 -5
- package/src/lib/tunnel.js +1 -0
- package/src/lib/worker-runtime.js +81 -0
- package/src/lib/workers/crypto-checksum-worker.js +70 -0
- package/src/modes/ai.js +11 -8
- package/src/modes/client.js +27 -15
- package/src/modes/doctor.js +11 -7
- package/src/modes/host.js +254 -99
- package/src/server.js +95 -18
package/src/modes/host.js
CHANGED
|
@@ -13,19 +13,21 @@
|
|
|
13
13
|
* ============================================================
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { execa
|
|
16
|
+
import { execa } from 'execa';
|
|
17
17
|
import chalk from 'chalk';
|
|
18
18
|
import inquirer from 'inquirer';
|
|
19
19
|
import path from 'node:path';
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
21
22
|
import fs from 'node:fs';
|
|
22
23
|
import os from 'node:os';
|
|
23
24
|
import { generateUID } from '../lib/uid.js';
|
|
24
|
-
import {
|
|
25
|
+
import { decryptAsync } from '../lib/crypto.js';
|
|
25
26
|
import { cleanupAll, killProcessTree, trackPID, untrackPID, setRevokeOnExit, addCleanupHook } from '../lib/cleanup.js';
|
|
26
27
|
import { detectOS } from '../lib/platform.js';
|
|
27
28
|
import { createSpinner, networkSpinner, typeText } from '../lib/animations.js';
|
|
28
29
|
import { startChatServer, openLocalChatUI } from '../lib/chat.js';
|
|
30
|
+
import { secureSensitive } from '../lib/secure-print.js';
|
|
29
31
|
import { spawnTunnelSupervised } from '../lib/tunnel.js';
|
|
30
32
|
import { decideApprovalRequest, fetchApprovalRequests, pingBroker, registerWithBroker, revokeUID } from '../lib/broker.js';
|
|
31
33
|
import { cleanupSessionLog, getSessionLogPath, initSessionLog, logSessionEvent, recordEvent } from '../lib/session-log.js';
|
|
@@ -55,24 +57,24 @@ async function ensureSSHRunning() {
|
|
|
55
57
|
try {
|
|
56
58
|
if (osInfo.isLinux) {
|
|
57
59
|
try {
|
|
58
|
-
await
|
|
60
|
+
await execa('systemctl', ['is-active', 'ssh'], { reject: true });
|
|
59
61
|
spinner.succeed('SSH service is active');
|
|
60
62
|
} catch {
|
|
61
63
|
spinner.text = 'Starting SSH service...';
|
|
62
64
|
try {
|
|
63
|
-
await
|
|
65
|
+
await execa('sudo', ['systemctl', 'start', 'ssh'], { stdio: 'inherit' });
|
|
64
66
|
spinner.succeed('SSH service started');
|
|
65
67
|
} catch {
|
|
66
|
-
await
|
|
68
|
+
await execa('sudo', ['systemctl', 'start', 'sshd'], { stdio: 'inherit' });
|
|
67
69
|
spinner.succeed('SSH service started (sshd)');
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
72
|
} else if (osInfo.isMac) {
|
|
71
73
|
try {
|
|
72
|
-
const { stdout } = await
|
|
74
|
+
const { stdout } = await execa('sudo', ['systemsetup', '-getremotelogin'], { reject: false });
|
|
73
75
|
if (stdout.toLowerCase().includes('off')) {
|
|
74
76
|
spinner.text = 'Enabling Remote Login...';
|
|
75
|
-
await
|
|
77
|
+
await execa('sudo', ['systemsetup', '-setremotelogin', 'on'], { stdio: 'inherit' });
|
|
76
78
|
spinner.succeed('Remote Login enabled');
|
|
77
79
|
} else {
|
|
78
80
|
spinner.succeed('SSH (Remote Login) is active');
|
|
@@ -82,10 +84,10 @@ async function ensureSSHRunning() {
|
|
|
82
84
|
}
|
|
83
85
|
} else if (osInfo.isWindows) {
|
|
84
86
|
try {
|
|
85
|
-
const { stdout } = await
|
|
87
|
+
const { stdout } = await execa('sc', ['query', 'sshd'], { reject: false });
|
|
86
88
|
if (stdout.includes('STOPPED')) {
|
|
87
89
|
spinner.text = 'Starting OpenSSH Server...';
|
|
88
|
-
await
|
|
90
|
+
await execa('net', ['start', 'sshd'], { stdio: 'inherit' });
|
|
89
91
|
spinner.succeed('OpenSSH Server started');
|
|
90
92
|
} else if (stdout.includes('RUNNING')) {
|
|
91
93
|
spinner.succeed('OpenSSH Server is running');
|
|
@@ -112,64 +114,34 @@ async function ensureTmuxInstalled() {
|
|
|
112
114
|
const spinner = createSpinner('Checking tmux installation...', networkSpinner).start();
|
|
113
115
|
try {
|
|
114
116
|
try {
|
|
115
|
-
await
|
|
117
|
+
await execa('tmux', ['-V'], { reject: true });
|
|
116
118
|
spinner.succeed('tmux is installed (Terminal Mirroring available)');
|
|
117
119
|
} catch {
|
|
118
120
|
spinner.text = 'tmux not found. Attempting to install...';
|
|
119
121
|
if (osInfo.isLinux) {
|
|
120
122
|
if (fs.existsSync('/usr/bin/apt') || fs.existsSync('/usr/bin/apt-get')) {
|
|
121
|
-
await
|
|
123
|
+
await execa('sudo', ['apt-get', 'update', '-qq'], { stdio: 'inherit' });
|
|
124
|
+
await execa('sudo', ['apt-get', 'install', '-y', 'tmux'], { stdio: 'inherit' });
|
|
122
125
|
} else if (fs.existsSync('/usr/bin/dnf')) {
|
|
123
|
-
await
|
|
126
|
+
await execa('sudo', ['dnf', 'install', '-y', 'tmux'], { stdio: 'inherit' });
|
|
124
127
|
} else if (fs.existsSync('/usr/bin/yum')) {
|
|
125
|
-
await
|
|
128
|
+
await execa('sudo', ['yum', 'install', '-y', 'tmux'], { stdio: 'inherit' });
|
|
126
129
|
} else if (fs.existsSync('/usr/bin/pacman')) {
|
|
127
|
-
await
|
|
130
|
+
await execa('sudo', ['pacman', '-S', '--noconfirm', 'tmux'], { stdio: 'inherit' });
|
|
128
131
|
} else if (fs.existsSync('/sbin/apk')) {
|
|
129
|
-
await
|
|
132
|
+
await execa('sudo', ['apk', 'add', 'tmux'], { stdio: 'inherit' });
|
|
130
133
|
} else {
|
|
131
134
|
throw new Error('Unsupported Linux package manager');
|
|
132
135
|
}
|
|
133
136
|
spinner.succeed('tmux installed successfully (Terminal Mirroring available)');
|
|
134
137
|
} else if (osInfo.isMac) {
|
|
135
138
|
try {
|
|
136
|
-
await
|
|
139
|
+
await execa('brew', ['install', 'tmux'], { stdio: 'inherit' });
|
|
137
140
|
spinner.succeed('tmux installed successfully (Terminal Mirroring available)');
|
|
138
141
|
} catch {
|
|
139
142
|
throw new Error('Homebrew is required to install tmux on macOS');
|
|
140
143
|
}
|
|
141
144
|
}
|
|
142
|
-
|
|
143
|
-
function isSecureLinkSession(name) {
|
|
144
|
-
return name === TMUX_SESSION_NAME || name.startsWith(TMUX_SESSION_PREFIX);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async function listTmuxSessions(socketArgs = []) {
|
|
148
|
-
const result = await execa('tmux', [...socketArgs, 'list-sessions', '-F', '#{session_name}|#{session_created}'], { reject: false });
|
|
149
|
-
if (result.exitCode !== 0) return [];
|
|
150
|
-
return result.stdout
|
|
151
|
-
.split(/\r?\n/)
|
|
152
|
-
.filter(Boolean)
|
|
153
|
-
.map(line => {
|
|
154
|
-
const [name, createdAt] = line.split('|');
|
|
155
|
-
return { name, createdAt: Number(createdAt) || null };
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async function getMirrorableSessions() {
|
|
160
|
-
const sessions = [];
|
|
161
|
-
const customSessions = await listTmuxSessions(tmuxSocketArgs());
|
|
162
|
-
customSessions
|
|
163
|
-
.filter(s => isSecureLinkSession(s.name))
|
|
164
|
-
.forEach(s => sessions.push({ ...s, socketArgs: tmuxSocketArgs(), source: 'custom' }));
|
|
165
|
-
|
|
166
|
-
const legacySessions = await listTmuxSessions();
|
|
167
|
-
legacySessions
|
|
168
|
-
.filter(s => isSecureLinkSession(s.name))
|
|
169
|
-
.forEach(s => sessions.push({ ...s, socketArgs: [], source: 'legacy' }));
|
|
170
|
-
|
|
171
|
-
return sessions;
|
|
172
|
-
}
|
|
173
145
|
}
|
|
174
146
|
} catch (err) {
|
|
175
147
|
spinner.fail(`tmux check/install failed: ${err.message}`);
|
|
@@ -177,6 +149,37 @@ async function ensureTmuxInstalled() {
|
|
|
177
149
|
}
|
|
178
150
|
}
|
|
179
151
|
|
|
152
|
+
function isSecureLinkSession(name) {
|
|
153
|
+
return name === TMUX_SESSION_NAME || name.startsWith(TMUX_SESSION_PREFIX);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function listTmuxSessions(socketArgs = []) {
|
|
157
|
+
const result = await execa('tmux', [...socketArgs, 'list-sessions', '-F', '#{session_name}|#{session_created}'], { reject: false });
|
|
158
|
+
if (result.exitCode !== 0) return [];
|
|
159
|
+
return result.stdout
|
|
160
|
+
.split(/\r?\n/)
|
|
161
|
+
.filter(Boolean)
|
|
162
|
+
.map(line => {
|
|
163
|
+
const [name, createdAt] = line.split('|');
|
|
164
|
+
return { name, createdAt: Number(createdAt) || null };
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function getMirrorableSessions() {
|
|
169
|
+
const sessions = [];
|
|
170
|
+
const customSessions = await listTmuxSessions(tmuxSocketArgs());
|
|
171
|
+
customSessions
|
|
172
|
+
.filter(s => isSecureLinkSession(s.name))
|
|
173
|
+
.forEach(s => sessions.push({ ...s, socketArgs: tmuxSocketArgs(), source: 'custom' }));
|
|
174
|
+
|
|
175
|
+
const legacySessions = await listTmuxSessions();
|
|
176
|
+
legacySessions
|
|
177
|
+
.filter(s => isSecureLinkSession(s.name))
|
|
178
|
+
.forEach(s => sessions.push({ ...s, socketArgs: [], source: 'legacy' }));
|
|
179
|
+
|
|
180
|
+
return sessions;
|
|
181
|
+
}
|
|
182
|
+
|
|
180
183
|
// ─── Ephemeral SSH Key Management ────────────────────────────
|
|
181
184
|
async function generateEphemeralKey() {
|
|
182
185
|
const tmpDir = os.tmpdir() || process.env.TMPDIR || process.env.TEMP || process.env.TMP;
|
|
@@ -275,6 +278,44 @@ async function startLocalHostDashboard(uid, password, serviceConfig, sessionStat
|
|
|
275
278
|
const { default: open } = await import('open');
|
|
276
279
|
const app = express();
|
|
277
280
|
const startedAt = new Date().toISOString();
|
|
281
|
+
const decryptedClientCache = new Map();
|
|
282
|
+
const MAX_DECRYPTED_CLIENT_CACHE = 100;
|
|
283
|
+
const activeEventStreams = new Set();
|
|
284
|
+
const MAX_EVENT_STREAMS = 5;
|
|
285
|
+
|
|
286
|
+
async function fetchDecryptedClients() {
|
|
287
|
+
const brokerRes = await fetch(`${BROKER_URL}/clients/${uid}`, {
|
|
288
|
+
headers: sessionState.hostToken ? { 'x-host-token': sessionState.hostToken } : {}
|
|
289
|
+
});
|
|
290
|
+
if (!brokerRes.ok) {
|
|
291
|
+
const data = await brokerRes.json().catch(() => ({}));
|
|
292
|
+
throw new Error(data.error || 'Failed to fetch clients');
|
|
293
|
+
}
|
|
294
|
+
const data = await brokerRes.json();
|
|
295
|
+
const activeCacheKeys = new Set();
|
|
296
|
+
const decryptedClients = await Promise.all((data.clients || []).map(async (clientBlob) => {
|
|
297
|
+
const cacheKey = `${clientBlob.iv}:${clientBlob.salt}:${clientBlob.ciphertext}`;
|
|
298
|
+
activeCacheKeys.add(cacheKey);
|
|
299
|
+
const cached = decryptedClientCache.get(cacheKey);
|
|
300
|
+
if (cached) return { ...cached, seenAt: clientBlob.seenAt || null };
|
|
301
|
+
try {
|
|
302
|
+
const decrypted = await decryptAsync(clientBlob.iv, clientBlob.ciphertext, password, clientBlob.salt);
|
|
303
|
+
const t = JSON.parse(decrypted);
|
|
304
|
+
decryptedClientCache.set(cacheKey, t);
|
|
305
|
+
return { ...t, seenAt: clientBlob.seenAt || null };
|
|
306
|
+
} catch {
|
|
307
|
+
const failed = { error: 'decrypt_failed' };
|
|
308
|
+
decryptedClientCache.set(cacheKey, failed);
|
|
309
|
+
return { ...failed, seenAt: clientBlob.seenAt || null };
|
|
310
|
+
}
|
|
311
|
+
}));
|
|
312
|
+
for (const key of decryptedClientCache.keys()) {
|
|
313
|
+
if (!activeCacheKeys.has(key) || decryptedClientCache.size > MAX_DECRYPTED_CLIENT_CACHE) {
|
|
314
|
+
decryptedClientCache.delete(key);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return { clients: decryptedClients };
|
|
318
|
+
}
|
|
278
319
|
|
|
279
320
|
app.get('/api/status', (_req, res) => {
|
|
280
321
|
res.json({
|
|
@@ -291,47 +332,91 @@ async function startLocalHostDashboard(uid, password, serviceConfig, sessionStat
|
|
|
291
332
|
|
|
292
333
|
app.use(express.json());
|
|
293
334
|
|
|
335
|
+
app.get('/api/approvals', async (_req, res) => {
|
|
336
|
+
try {
|
|
337
|
+
const data = await fetchApprovalRequests(BROKER_URL, uid, sessionState.hostToken);
|
|
338
|
+
res.json(data);
|
|
339
|
+
} catch (err) {
|
|
340
|
+
res.status(500).json({ error: err.message });
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
294
344
|
// Server-Sent Events for live telemetry & approvals
|
|
295
345
|
app.get('/api/events', async (req, res) => {
|
|
346
|
+
if (activeEventStreams.size >= MAX_EVENT_STREAMS) {
|
|
347
|
+
return res.status(503).json({ error: 'Too many dashboard event streams' });
|
|
348
|
+
}
|
|
349
|
+
activeEventStreams.add(res);
|
|
296
350
|
res.set({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' });
|
|
297
351
|
res.flushHeaders?.();
|
|
298
352
|
|
|
299
353
|
let closed = false;
|
|
300
|
-
|
|
354
|
+
let timer = null;
|
|
355
|
+
let intervalMs = 5000;
|
|
356
|
+
let unchangedCycles = 0;
|
|
357
|
+
let lastApprovals = '';
|
|
358
|
+
let lastClients = '';
|
|
359
|
+
|
|
360
|
+
const writeEvent = (event, payload) => {
|
|
361
|
+
if (closed) return;
|
|
362
|
+
res.write(`event: ${event}\n`);
|
|
363
|
+
res.write(`data: ${JSON.stringify(payload)}\n\n`);
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const scheduleNext = () => {
|
|
367
|
+
if (closed) return;
|
|
368
|
+
timer = setTimeout(pushLoop, intervalMs);
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const pushLoop = async () => {
|
|
301
372
|
if (closed) return;
|
|
302
373
|
try {
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
374
|
+
const [approvalData, clientData] = await Promise.all([
|
|
375
|
+
fetchApprovalRequests(BROKER_URL, uid, sessionState.hostToken).catch(() => ({ approvals: [] })),
|
|
376
|
+
fetchDecryptedClients().catch(() => ({ clients: [] })),
|
|
377
|
+
]);
|
|
378
|
+
|
|
379
|
+
const approvalsPayload = { approvals: approvalData.approvals || [] };
|
|
380
|
+
const clientsPayload = { clients: clientData.clients || [] };
|
|
381
|
+
const approvalsHash = JSON.stringify(approvalsPayload.approvals);
|
|
382
|
+
const clientsHash = JSON.stringify(clientsPayload.clients);
|
|
383
|
+
const changed = approvalsHash !== lastApprovals || clientsHash !== lastClients;
|
|
384
|
+
|
|
385
|
+
if (approvalsHash !== lastApprovals) {
|
|
386
|
+
writeEvent('approvals', approvalsPayload);
|
|
387
|
+
lastApprovals = approvalsHash;
|
|
388
|
+
}
|
|
389
|
+
if (clientsHash !== lastClients) {
|
|
390
|
+
writeEvent('clients', clientsPayload);
|
|
391
|
+
lastClients = clientsHash;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (changed) {
|
|
395
|
+
unchangedCycles = 0;
|
|
396
|
+
intervalMs = 5000;
|
|
397
|
+
} else {
|
|
398
|
+
unchangedCycles += 1;
|
|
399
|
+
intervalMs = Math.min(20000, 5000 * (2 ** Math.min(2, unchangedCycles)));
|
|
400
|
+
}
|
|
401
|
+
} finally {
|
|
402
|
+
scheduleNext();
|
|
403
|
+
}
|
|
307
404
|
};
|
|
308
405
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
406
|
+
req.on('close', () => {
|
|
407
|
+
closed = true;
|
|
408
|
+
activeEventStreams.delete(res);
|
|
409
|
+
if (timer) clearTimeout(timer);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
writeEvent('ready', { ok: true });
|
|
413
|
+
await pushLoop();
|
|
313
414
|
});
|
|
314
415
|
|
|
315
416
|
app.get('/api/clients', async (_req, res) => {
|
|
316
417
|
try {
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
});
|
|
320
|
-
if (!brokerRes.ok) {
|
|
321
|
-
const data = await brokerRes.json().catch(() => ({}));
|
|
322
|
-
return res.status(brokerRes.status).json({ error: data.error || 'Failed to fetch clients' });
|
|
323
|
-
}
|
|
324
|
-
const data = await brokerRes.json();
|
|
325
|
-
const clients = (data.clients || []).map((clientBlob) => {
|
|
326
|
-
try {
|
|
327
|
-
const decrypted = decrypt(clientBlob.iv, clientBlob.ciphertext, password, clientBlob.salt);
|
|
328
|
-
const t = JSON.parse(decrypted);
|
|
329
|
-
return { ...t, seenAt: clientBlob.seenAt || null };
|
|
330
|
-
} catch {
|
|
331
|
-
return { error: 'decrypt_failed', seenAt: clientBlob.seenAt || null };
|
|
332
|
-
}
|
|
333
|
-
});
|
|
334
|
-
res.json({ clients });
|
|
418
|
+
const clients = await fetchDecryptedClients();
|
|
419
|
+
res.json(clients);
|
|
335
420
|
} catch (err) {
|
|
336
421
|
res.status(500).json({ error: err.message });
|
|
337
422
|
}
|
|
@@ -484,12 +569,44 @@ updateUptime();
|
|
|
484
569
|
|
|
485
570
|
// SSE for live updates
|
|
486
571
|
const es = new EventSource('/api/events');
|
|
572
|
+
let fallbackTimer = null;
|
|
573
|
+
let fallbackDelay = 5000;
|
|
574
|
+
|
|
575
|
+
function scheduleFallbackPoll() {
|
|
576
|
+
if (fallbackTimer) return;
|
|
577
|
+
fallbackTimer = setTimeout(async function pollFallback() {
|
|
578
|
+
fallbackTimer = null;
|
|
579
|
+
try {
|
|
580
|
+
const [approvalsRes, clientsRes] = await Promise.all([
|
|
581
|
+
fetch('/api/approvals'),
|
|
582
|
+
fetch('/api/clients')
|
|
583
|
+
]);
|
|
584
|
+
if (approvalsRes.ok) renderApprovals((await approvalsRes.json()).approvals || []);
|
|
585
|
+
if (clientsRes.ok) renderClients((await clientsRes.json()).clients || []);
|
|
586
|
+
} catch {}
|
|
587
|
+
fallbackDelay = Math.min(20000, fallbackDelay * 2);
|
|
588
|
+
scheduleFallbackPoll();
|
|
589
|
+
}, fallbackDelay);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
es.addEventListener('open', () => {
|
|
593
|
+
fallbackDelay = 5000;
|
|
594
|
+
if (fallbackTimer) clearTimeout(fallbackTimer);
|
|
595
|
+
fallbackTimer = null;
|
|
596
|
+
});
|
|
597
|
+
es.addEventListener('error', scheduleFallbackPoll);
|
|
487
598
|
es.addEventListener('approvals', (e) => {
|
|
488
599
|
try {
|
|
489
600
|
const data = JSON.parse(e.data);
|
|
490
601
|
renderApprovals(data.approvals || []);
|
|
491
602
|
} catch {}
|
|
492
603
|
});
|
|
604
|
+
es.addEventListener('clients', (e) => {
|
|
605
|
+
try {
|
|
606
|
+
const data = JSON.parse(e.data);
|
|
607
|
+
renderClients(data.clients || []);
|
|
608
|
+
} catch {}
|
|
609
|
+
});
|
|
493
610
|
|
|
494
611
|
function renderApprovals(approvals) {
|
|
495
612
|
const container = document.getElementById('approvals');
|
|
@@ -554,16 +671,6 @@ async function revokeSession() {
|
|
|
554
671
|
} catch (err) { showToast('Error: ' + err.message); }
|
|
555
672
|
}
|
|
556
673
|
|
|
557
|
-
// Poll client telemetry (separate from SSE for simplicity)
|
|
558
|
-
async function pollClients() {
|
|
559
|
-
try {
|
|
560
|
-
const res = await fetch('/api/clients');
|
|
561
|
-
if (!res.ok) return;
|
|
562
|
-
const data = await res.json();
|
|
563
|
-
renderClients(data.clients || []);
|
|
564
|
-
} catch {}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
674
|
function renderClients(clients) {
|
|
568
675
|
const container = document.getElementById('clients');
|
|
569
676
|
if (!clients || clients.length === 0) {
|
|
@@ -593,8 +700,6 @@ function renderClients(clients) {
|
|
|
593
700
|
container.innerHTML = html;
|
|
594
701
|
}
|
|
595
702
|
|
|
596
|
-
setInterval(pollClients, 5000);
|
|
597
|
-
pollClients();
|
|
598
703
|
</script>
|
|
599
704
|
</body></html>`);
|
|
600
705
|
});
|
|
@@ -616,18 +721,67 @@ pollClients();
|
|
|
616
721
|
async function spawnPrivateBroker() {
|
|
617
722
|
console.log(chalk.yellow('\n ⚠️ Public Broker is unreachable. Spawning Private Broker...'));
|
|
618
723
|
|
|
724
|
+
const packageRoot = path.resolve(__dirname, '../..');
|
|
725
|
+
const serverEntrypoint = path.join(__dirname, '../server.js');
|
|
726
|
+
const requireFromServer = createRequire(serverEntrypoint);
|
|
727
|
+
const requiredBrokerPackages = ['express', 'express-rate-limit', 'helmet'];
|
|
728
|
+
const missingPackages = requiredBrokerPackages.filter((pkg) => {
|
|
729
|
+
try {
|
|
730
|
+
requireFromServer.resolve(pkg);
|
|
731
|
+
return false;
|
|
732
|
+
} catch {
|
|
733
|
+
return true;
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
if (missingPackages.length > 0) {
|
|
738
|
+
console.log(chalk.yellow(` ⚠️ Missing broker runtime packages: ${missingPackages.join(', ')}`));
|
|
739
|
+
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
740
|
+
const installResult = await execa(
|
|
741
|
+
npmCmd,
|
|
742
|
+
['install', '--no-save', '--no-audit', '--no-fund', ...missingPackages],
|
|
743
|
+
{ cwd: packageRoot, reject: false, all: true }
|
|
744
|
+
);
|
|
745
|
+
|
|
746
|
+
if (installResult.exitCode !== 0) {
|
|
747
|
+
const installOutput = installResult.all?.trim();
|
|
748
|
+
throw new Error(
|
|
749
|
+
`Private broker dependencies failed to install (${missingPackages.join(', ')})`
|
|
750
|
+
+ (installOutput ? `: ${installOutput}` : '')
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Verify modules are now resolvable for the server entrypoint context.
|
|
755
|
+
const unresolved = missingPackages.filter((pkg) => {
|
|
756
|
+
try {
|
|
757
|
+
requireFromServer.resolve(pkg);
|
|
758
|
+
return false;
|
|
759
|
+
} catch {
|
|
760
|
+
return true;
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
if (unresolved.length > 0) {
|
|
764
|
+
throw new Error(`Private broker dependencies are still missing after install: ${unresolved.join(', ')}`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
619
768
|
// 1. Spawn the broker server process
|
|
620
|
-
const brokerProcess = execa('node', [
|
|
769
|
+
const brokerProcess = execa('node', [serverEntrypoint], {
|
|
621
770
|
env: { ...process.env, PORT: '4040' },
|
|
622
771
|
reject: false,
|
|
623
772
|
all: true,
|
|
773
|
+
buffer: false,
|
|
624
774
|
});
|
|
625
775
|
trackPID(brokerProcess.pid);
|
|
626
776
|
|
|
627
777
|
let brokerExited = false;
|
|
628
|
-
|
|
778
|
+
const maxBrokerOutputBytes = 64 * 1024;
|
|
779
|
+
let brokerOutput = Buffer.alloc(0);
|
|
629
780
|
brokerProcess.all?.on('data', chunk => {
|
|
630
|
-
|
|
781
|
+
const next = Buffer.concat([brokerOutput, Buffer.from(chunk)]);
|
|
782
|
+
brokerOutput = next.length > maxBrokerOutputBytes
|
|
783
|
+
? next.subarray(next.length - maxBrokerOutputBytes)
|
|
784
|
+
: next;
|
|
631
785
|
});
|
|
632
786
|
brokerProcess.on('exit', () => {
|
|
633
787
|
brokerExited = true;
|
|
@@ -641,7 +795,8 @@ async function spawnPrivateBroker() {
|
|
|
641
795
|
|
|
642
796
|
await waitForValue(() => {
|
|
643
797
|
if (brokerExited) {
|
|
644
|
-
|
|
798
|
+
const output = brokerOutput.toString('utf8').trim();
|
|
799
|
+
throw new Error(`Private broker exited before tunnel was ready${output ? `: ${output}` : ''}`);
|
|
645
800
|
}
|
|
646
801
|
return brokerTunnelUrl;
|
|
647
802
|
}, 30000, 'Private broker tunnel startup');
|
|
@@ -674,7 +829,7 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
674
829
|
console.log(chalk.bold(' ║ 🛡️ SecureLink — HOST MODE ACTIVE ║'));
|
|
675
830
|
console.log(chalk.bold(' ╠════════════════════════════════════════════════════╣'));
|
|
676
831
|
console.log(` ║ ${chalk.cyan('UID:')} ${chalk.bold.white(uid.padEnd(30))}║`);
|
|
677
|
-
console.log(` ║ ${chalk.cyan('Password:')} ${chalk.bold.white(password.padEnd(30))}║`);
|
|
832
|
+
console.log(` ║ ${chalk.cyan('Password:')} ${chalk.bold.white(secureSensitive(password).padEnd(30))}║`);
|
|
678
833
|
console.log(` ║ ${chalk.cyan('Service:')} ${chalk.dim(serviceConfig.type.toUpperCase() + ' (Port ' + serviceConfig.port + ')').padEnd(30)}║`);
|
|
679
834
|
console.log(` ║ ${chalk.cyan('Tunnel:')} ${chalk.dim(sessionState.tunnelUrl.substring(0, 40))} ║`);
|
|
680
835
|
if (serviceConfig.chatUrl) {
|
|
@@ -751,7 +906,7 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
751
906
|
for (const request of pending) {
|
|
752
907
|
let details = {};
|
|
753
908
|
try {
|
|
754
|
-
details = JSON.parse(
|
|
909
|
+
details = JSON.parse(await decryptAsync(request.iv, request.ciphertext, password, request.salt));
|
|
755
910
|
} catch {
|
|
756
911
|
details = { error: 'Could not decrypt request metadata' };
|
|
757
912
|
}
|
|
@@ -841,7 +996,7 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
841
996
|
console.log('');
|
|
842
997
|
|
|
843
998
|
try {
|
|
844
|
-
await
|
|
999
|
+
await execa('tmux', ['-V'], { reject: true });
|
|
845
1000
|
const sessions = await getMirrorableSessions();
|
|
846
1001
|
if (sessions.length === 0) {
|
|
847
1002
|
console.log(chalk.yellow(' ⚠️ No mirrored terminal session is active yet.'));
|
|
@@ -891,10 +1046,10 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
891
1046
|
} else {
|
|
892
1047
|
spinner.succeed(`Found ${data.clients.length} recent connection(s):`);
|
|
893
1048
|
|
|
894
|
-
data.clients.
|
|
1049
|
+
for (const [i, clientBlob] of data.clients.entries()) {
|
|
895
1050
|
try {
|
|
896
1051
|
// Decrypt using the unique salt the client generated for this payload
|
|
897
|
-
const decrypted =
|
|
1052
|
+
const decrypted = await decryptAsync(clientBlob.iv, clientBlob.ciphertext, password, clientBlob.salt);
|
|
898
1053
|
const t = JSON.parse(decrypted);
|
|
899
1054
|
|
|
900
1055
|
console.log(chalk.bold.blue(`\n Client #${i + 1} (${t.username})`));
|
|
@@ -908,7 +1063,7 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
908
1063
|
console.log(chalk.yellow(`\n Client #${i + 1}: Payload decryption failed (wrong password or corrupted).`));
|
|
909
1064
|
logSessionEvent('host_telemetry_decrypt_failed', { index: i + 1 }, 'warn');
|
|
910
1065
|
}
|
|
911
|
-
}
|
|
1066
|
+
}
|
|
912
1067
|
}
|
|
913
1068
|
} catch (e) {
|
|
914
1069
|
spinner.fail('Could not reach broker.');
|
|
@@ -928,9 +1083,9 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
928
1083
|
const spinner = createSpinner('Terminating active SSH sessions...', networkSpinner).start();
|
|
929
1084
|
try {
|
|
930
1085
|
if (process.platform === 'win32') {
|
|
931
|
-
await
|
|
1086
|
+
await execa('powershell', ['-NoProfile', '-Command', "Get-CimInstance Win32_Process -Filter \"name = 'sshd.exe'\" | Where-Object { $_.CommandLine -match 'sshd:.*@' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }"], { reject: false });
|
|
932
1087
|
} else {
|
|
933
|
-
await
|
|
1088
|
+
await execa('pkill', ['-f', 'sshd:.*@'], { reject: false });
|
|
934
1089
|
await execa('tmux', [...tmuxSocketArgs(), 'kill-server'], { reject: false });
|
|
935
1090
|
const legacySessions = await listTmuxSessions();
|
|
936
1091
|
for (const session of legacySessions) {
|
|
@@ -994,7 +1149,7 @@ export async function startHostMode() {
|
|
|
994
1149
|
},
|
|
995
1150
|
]);
|
|
996
1151
|
const password = pwdInput.trim() || generateUID();
|
|
997
|
-
console.log(` ${chalk.green('✓')} Password: ${chalk.bold.white(password)}`);
|
|
1152
|
+
console.log(` ${chalk.green('✓')} Password: ${chalk.bold.white(secureSensitive(password))}`);
|
|
998
1153
|
console.log('');
|
|
999
1154
|
|
|
1000
1155
|
// ─── Broker Selection ───
|