@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/src/modes/host.js
CHANGED
|
@@ -21,7 +21,9 @@ import { fileURLToPath } from 'node:url';
|
|
|
21
21
|
import { createRequire } from 'node:module';
|
|
22
22
|
import fs from 'node:fs';
|
|
23
23
|
import os from 'node:os';
|
|
24
|
+
import crypto from 'node:crypto';
|
|
24
25
|
import { generateUID } from '../lib/uid.js';
|
|
26
|
+
import { openUrl } from '../lib/open-url.js';
|
|
25
27
|
import { decryptAsync } from '../lib/crypto.js';
|
|
26
28
|
import { cleanupAll, killProcessTree, trackPID, untrackPID, setRevokeOnExit, addCleanupHook } from '../lib/cleanup.js';
|
|
27
29
|
import { detectOS } from '../lib/platform.js';
|
|
@@ -36,6 +38,16 @@ import { TMUX_SESSION_NAME, TMUX_SESSION_PREFIX, tmuxSocketArgs } from '../lib/t
|
|
|
36
38
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
37
39
|
let BROKER_URL = process.env.BROKER_URL || 'https://ipingyou.onrender.com';
|
|
38
40
|
|
|
41
|
+
function escapeDashboardHtml(value) {
|
|
42
|
+
return String(value ?? '').replace(/[&<>"']/g, character => ({
|
|
43
|
+
'&': '&',
|
|
44
|
+
'<': '<',
|
|
45
|
+
'>': '>',
|
|
46
|
+
'"': '"',
|
|
47
|
+
"'": ''',
|
|
48
|
+
})[character]);
|
|
49
|
+
}
|
|
50
|
+
|
|
39
51
|
async function waitForValue(getValue, timeoutMs, label) {
|
|
40
52
|
const startedAt = Date.now();
|
|
41
53
|
while (!getValue()) {
|
|
@@ -209,15 +221,26 @@ async function injectPublicKey(pubKey) {
|
|
|
209
221
|
}
|
|
210
222
|
|
|
211
223
|
const authKeysPath = path.join(sshDir, 'authorized_keys');
|
|
212
|
-
await fs.promises.
|
|
213
|
-
|
|
224
|
+
const existing = await fs.promises.lstat(authKeysPath).catch(() => null);
|
|
225
|
+
if (existing?.isSymbolicLink()) {
|
|
226
|
+
throw new Error('Refusing to modify a symlinked authorized_keys file');
|
|
227
|
+
}
|
|
228
|
+
const authorizedKey = `no-agent-forwarding,no-X11-forwarding ${pubKey}`;
|
|
229
|
+
await fs.promises.appendFile(authKeysPath, `\n${authorizedKey}\n`, { mode: 0o600 });
|
|
230
|
+
await fs.promises.chmod(authKeysPath, 0o600);
|
|
231
|
+
return { authKeysPath, authorizedKey };
|
|
214
232
|
}
|
|
215
233
|
|
|
216
|
-
async function removePublicKey(authKeysPath,
|
|
234
|
+
async function removePublicKey(authKeysPath, authorizedKey) {
|
|
217
235
|
if (fs.existsSync(authKeysPath)) {
|
|
236
|
+
const stat = await fs.promises.lstat(authKeysPath);
|
|
237
|
+
if (stat.isSymbolicLink()) {
|
|
238
|
+
throw new Error('Refusing to modify a symlinked authorized_keys file');
|
|
239
|
+
}
|
|
218
240
|
let keys = await fs.promises.readFile(authKeysPath, 'utf8');
|
|
219
|
-
keys = keys.replace(`\n${
|
|
241
|
+
keys = keys.replace(`\n${authorizedKey}\n`, '');
|
|
220
242
|
await fs.promises.writeFile(authKeysPath, keys);
|
|
243
|
+
await fs.promises.chmod(authKeysPath, 0o600);
|
|
221
244
|
}
|
|
222
245
|
}
|
|
223
246
|
|
|
@@ -275,13 +298,17 @@ async function promptOneTimeSharePath() {
|
|
|
275
298
|
|
|
276
299
|
async function startLocalHostDashboard(uid, password, serviceConfig, sessionState) {
|
|
277
300
|
const { default: express } = await import('express');
|
|
278
|
-
const { default: open } = await import('open');
|
|
279
301
|
const app = express();
|
|
280
302
|
const startedAt = new Date().toISOString();
|
|
281
303
|
const decryptedClientCache = new Map();
|
|
282
304
|
const MAX_DECRYPTED_CLIENT_CACHE = 100;
|
|
283
305
|
const activeEventStreams = new Set();
|
|
284
306
|
const MAX_EVENT_STREAMS = 5;
|
|
307
|
+
const dashboardUid = escapeDashboardHtml(uid);
|
|
308
|
+
const dashboardService = escapeDashboardHtml(String(serviceConfig.type || '').toUpperCase());
|
|
309
|
+
const dashboardPort = escapeDashboardHtml(serviceConfig.port);
|
|
310
|
+
const dashboardDropPath = escapeDashboardHtml(serviceConfig.sharedDropPath || 'none');
|
|
311
|
+
const dashboardSharePath = escapeDashboardHtml(serviceConfig.oneTimeSharePath || 'none');
|
|
285
312
|
|
|
286
313
|
async function fetchDecryptedClients() {
|
|
287
314
|
const brokerRes = await fetch(`${BROKER_URL}/clients/${uid}`, {
|
|
@@ -317,6 +344,22 @@ async function startLocalHostDashboard(uid, password, serviceConfig, sessionStat
|
|
|
317
344
|
return { clients: decryptedClients };
|
|
318
345
|
}
|
|
319
346
|
|
|
347
|
+
async function fetchDecryptedApprovals() {
|
|
348
|
+
const data = await fetchApprovalRequests(BROKER_URL, uid, sessionState.hostToken);
|
|
349
|
+
const decryptedApprovals = await Promise.all((data.approvals || []).map(async (a) => {
|
|
350
|
+
const base = { id: a.id, status: a.status, createdAt: a.createdAt, decidedAt: a.decidedAt };
|
|
351
|
+
if (!a.iv || !a.ciphertext || !a.salt) return base;
|
|
352
|
+
try {
|
|
353
|
+
const decrypted = await decryptAsync(a.iv, a.ciphertext, password, a.salt);
|
|
354
|
+
const details = JSON.parse(decrypted);
|
|
355
|
+
return { ...base, username: details.username, hostname: details.hostname, os: details.os, intent: details.intent };
|
|
356
|
+
} catch {
|
|
357
|
+
return base;
|
|
358
|
+
}
|
|
359
|
+
}));
|
|
360
|
+
return { approvalRequired: data.approvalRequired, approvals: decryptedApprovals };
|
|
361
|
+
}
|
|
362
|
+
|
|
320
363
|
app.get('/api/status', (_req, res) => {
|
|
321
364
|
res.json({
|
|
322
365
|
uid,
|
|
@@ -334,7 +377,7 @@ async function startLocalHostDashboard(uid, password, serviceConfig, sessionStat
|
|
|
334
377
|
|
|
335
378
|
app.get('/api/approvals', async (_req, res) => {
|
|
336
379
|
try {
|
|
337
|
-
const data = await
|
|
380
|
+
const data = await fetchDecryptedApprovals();
|
|
338
381
|
res.json(data);
|
|
339
382
|
} catch (err) {
|
|
340
383
|
res.status(500).json({ error: err.message });
|
|
@@ -372,7 +415,7 @@ async function startLocalHostDashboard(uid, password, serviceConfig, sessionStat
|
|
|
372
415
|
if (closed) return;
|
|
373
416
|
try {
|
|
374
417
|
const [approvalData, clientData] = await Promise.all([
|
|
375
|
-
|
|
418
|
+
fetchDecryptedApprovals().catch(() => ({ approvals: [] })),
|
|
376
419
|
fetchDecryptedClients().catch(() => ({ clients: [] })),
|
|
377
420
|
]);
|
|
378
421
|
|
|
@@ -464,6 +507,17 @@ async function startLocalHostDashboard(uid, password, serviceConfig, sessionStat
|
|
|
464
507
|
});
|
|
465
508
|
|
|
466
509
|
app.get('/', (_req, res) => {
|
|
510
|
+
const scriptNonce = crypto.randomBytes(18).toString('base64');
|
|
511
|
+
res.set('Content-Security-Policy', [
|
|
512
|
+
"default-src 'self'",
|
|
513
|
+
`script-src 'nonce-${scriptNonce}'`,
|
|
514
|
+
"style-src 'self' 'unsafe-inline'",
|
|
515
|
+
"connect-src 'self'",
|
|
516
|
+
"img-src 'none'",
|
|
517
|
+
"object-src 'none'",
|
|
518
|
+
"base-uri 'none'",
|
|
519
|
+
"frame-ancestors 'none'",
|
|
520
|
+
].join('; '));
|
|
467
521
|
res.type('html').send(`<!doctype html>
|
|
468
522
|
<html lang="en"><head><meta charset="utf-8"><title>iPingYou Host Dashboard</title>
|
|
469
523
|
<style>
|
|
@@ -504,22 +558,22 @@ code{background:var(--bg);padding:2px 8px;border-radius:4px;font-size:0.85rem}
|
|
|
504
558
|
</style></head>
|
|
505
559
|
<body>
|
|
506
560
|
<div class="header">
|
|
507
|
-
<div><h1>🛡️ iPingYou Host Dashboard</h1><span style="color:var(--dim);font-size:0.85rem">UID: <code>${
|
|
561
|
+
<div><h1>🛡️ iPingYou Host Dashboard</h1><span style="color:var(--dim);font-size:0.85rem">UID: <code>${dashboardUid}</code></span></div>
|
|
508
562
|
<div style="display:flex;gap:1rem;align-items:center">
|
|
509
563
|
<span class="badge">● LIVE</span>
|
|
510
|
-
<button class="btn btn-revoke"
|
|
564
|
+
<button id="revoke-session" class="btn btn-revoke" type="button">🚫 Revoke Session</button>
|
|
511
565
|
</div>
|
|
512
566
|
</div>
|
|
513
567
|
|
|
514
568
|
<div class="grid">
|
|
515
569
|
<div class="card">
|
|
516
570
|
<h2>📊 Session Info</h2>
|
|
517
|
-
<div class="info-row"><span class="info-label">UID</span><span class="info-value"><code>${
|
|
571
|
+
<div class="info-row"><span class="info-label">UID</span><span class="info-value"><code>${dashboardUid}</code></span></div>
|
|
518
572
|
<div class="info-row"><span class="info-label">Password</span><span class="info-value"><code>[Hidden — see terminal]</code></span></div>
|
|
519
|
-
<div class="info-row"><span class="info-label">Service</span><span class="info-value">${
|
|
573
|
+
<div class="info-row"><span class="info-label">Service</span><span class="info-value">${dashboardService} on port ${dashboardPort}</span></div>
|
|
520
574
|
<div class="info-row"><span class="info-label">Approval Gate</span><span class="info-value">${serviceConfig.approvalRequired ? '<span style="color:var(--green)">✓ Enabled</span>' : '<span style="color:var(--dim)">Disabled</span>'}</span></div>
|
|
521
|
-
<div class="info-row"><span class="info-label">Drop Folder</span><span class="info-value"><code>${
|
|
522
|
-
<div class="info-row"><span class="info-label">One-Time Share</span><span class="info-value"><code>${
|
|
575
|
+
<div class="info-row"><span class="info-label">Drop Folder</span><span class="info-value"><code>${dashboardDropPath}</code></span></div>
|
|
576
|
+
<div class="info-row"><span class="info-label">One-Time Share</span><span class="info-value"><code>${dashboardSharePath}</code></span></div>
|
|
523
577
|
<div class="info-row"><span class="info-label">Chat</span><span class="info-value">${serviceConfig.chatUrl ? '<span style="color:var(--green)">Active</span>' : '<span style="color:var(--dim)">Not started</span>'}</span></div>
|
|
524
578
|
<div class="info-row"><span class="info-label">Uptime</span><span class="info-value" id="uptime">—</span></div>
|
|
525
579
|
</div>
|
|
@@ -537,7 +591,7 @@ code{background:var(--bg);padding:2px 8px;border-radius:4px;font-size:0.85rem}
|
|
|
537
591
|
|
|
538
592
|
<div id="toast"></div>
|
|
539
593
|
|
|
540
|
-
<script>
|
|
594
|
+
<script nonce="${scriptNonce}">
|
|
541
595
|
const startedAt = new Date("${startedAt}");
|
|
542
596
|
|
|
543
597
|
function showToast(msg) {
|
|
@@ -547,14 +601,11 @@ function showToast(msg) {
|
|
|
547
601
|
setTimeout(() => t.classList.remove('show'), 2500);
|
|
548
602
|
}
|
|
549
603
|
|
|
550
|
-
function
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
'"': '"',
|
|
556
|
-
"'": ''',
|
|
557
|
-
})[m]);
|
|
604
|
+
function appendEmptyState(container, text) {
|
|
605
|
+
const message = document.createElement('p');
|
|
606
|
+
message.className = 'empty';
|
|
607
|
+
message.textContent = text;
|
|
608
|
+
container.replaceChildren(message);
|
|
558
609
|
}
|
|
559
610
|
|
|
560
611
|
function updateUptime() {
|
|
@@ -615,39 +666,66 @@ function renderApprovals(approvals) {
|
|
|
615
666
|
document.getElementById('approval-count').textContent = pending.length > 0 ? '(' + pending.length + ' pending)' : '';
|
|
616
667
|
|
|
617
668
|
if (approvals.length === 0) {
|
|
618
|
-
container
|
|
669
|
+
appendEmptyState(container, 'No approval requests yet');
|
|
619
670
|
return;
|
|
620
671
|
}
|
|
621
672
|
|
|
622
|
-
|
|
673
|
+
const fragment = document.createDocumentFragment();
|
|
623
674
|
for (const req of pending) {
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
675
|
+
const item = document.createElement('div');
|
|
676
|
+
item.className = 'approval-item';
|
|
677
|
+
const heading = document.createElement('div');
|
|
678
|
+
heading.style.cssText = 'display:flex;justify-content:space-between;align-items:center';
|
|
679
|
+
const title = document.createElement('strong');
|
|
680
|
+
title.textContent = 'Request ' + String(req.id || '');
|
|
681
|
+
const badge = document.createElement('span');
|
|
682
|
+
badge.className = 'status-badge status-pending';
|
|
683
|
+
badge.textContent = 'PENDING';
|
|
684
|
+
heading.append(title, badge);
|
|
685
|
+
const details = document.createElement('div');
|
|
686
|
+
details.className = 'meta';
|
|
687
|
+
details.textContent = 'User: ' + String(req.username || 'unknown') + ' | Host: ' + String(req.hostname || 'unknown') + ' | OS: ' + String(req.os || 'unknown');
|
|
688
|
+
const meta = document.createElement('div');
|
|
689
|
+
meta.className = 'meta';
|
|
690
|
+
meta.textContent = 'Submitted: ' + new Date(req.createdAt).toLocaleTimeString();
|
|
691
|
+
const actions = document.createElement('div');
|
|
692
|
+
actions.className = 'actions';
|
|
693
|
+
for (const choice of [
|
|
694
|
+
{ decision: 'approved', label: '✅ Approve', className: 'btn btn-approve' },
|
|
695
|
+
{ decision: 'denied', label: '❌ Deny', className: 'btn btn-deny' }
|
|
696
|
+
]) {
|
|
697
|
+
const button = document.createElement('button');
|
|
698
|
+
button.className = choice.className;
|
|
699
|
+
button.type = 'button';
|
|
700
|
+
button.textContent = choice.label;
|
|
701
|
+
button.addEventListener('click', function() {
|
|
702
|
+
decide(String(req.id || ''), choice.decision);
|
|
703
|
+
});
|
|
704
|
+
actions.appendChild(button);
|
|
705
|
+
}
|
|
706
|
+
item.append(heading, details, meta, actions);
|
|
707
|
+
fragment.appendChild(item);
|
|
633
708
|
}
|
|
634
709
|
for (const req of decided) {
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
710
|
+
const status = req.status === 'approved' ? 'approved' : 'denied';
|
|
711
|
+
const item = document.createElement('div');
|
|
712
|
+
item.className = 'approval-item';
|
|
713
|
+
item.style.opacity = '0.6';
|
|
714
|
+
const heading = document.createElement('div');
|
|
715
|
+
heading.style.cssText = 'display:flex;justify-content:space-between;align-items:center';
|
|
716
|
+
const title = document.createElement('strong');
|
|
717
|
+
title.textContent = 'Request ' + String(req.id || '');
|
|
718
|
+
const badge = document.createElement('span');
|
|
719
|
+
badge.className = 'status-badge status-' + status;
|
|
720
|
+
badge.textContent = status.toUpperCase();
|
|
721
|
+
heading.append(title, badge);
|
|
722
|
+
const meta = document.createElement('div');
|
|
723
|
+
meta.className = 'meta';
|
|
724
|
+
meta.textContent = 'Decided: ' + (req.decidedAt ? new Date(req.decidedAt).toLocaleTimeString() : 'N/A');
|
|
725
|
+
item.append(heading, meta);
|
|
726
|
+
fragment.appendChild(item);
|
|
642
727
|
}
|
|
643
|
-
container.
|
|
644
|
-
|
|
645
|
-
// Attach click handlers via event delegation (avoids inline quote escaping issues)
|
|
646
|
-
container.querySelectorAll('[data-decision]').forEach(function(btn) {
|
|
647
|
-
btn.addEventListener('click', function() {
|
|
648
|
-
decide(btn.getAttribute('data-id'), btn.getAttribute('data-decision'));
|
|
649
|
-
});
|
|
650
|
-
});
|
|
728
|
+
container.replaceChildren(fragment);
|
|
651
729
|
}
|
|
652
730
|
|
|
653
731
|
async function decide(requestId, decision) {
|
|
@@ -670,34 +748,48 @@ async function revokeSession() {
|
|
|
670
748
|
else showToast('Failed to revoke');
|
|
671
749
|
} catch (err) { showToast('Error: ' + err.message); }
|
|
672
750
|
}
|
|
751
|
+
document.getElementById('revoke-session').addEventListener('click', revokeSession);
|
|
673
752
|
|
|
674
753
|
function renderClients(clients) {
|
|
675
754
|
const container = document.getElementById('clients');
|
|
676
755
|
if (!clients || clients.length === 0) {
|
|
677
|
-
container
|
|
756
|
+
appendEmptyState(container, 'No clients connected yet');
|
|
678
757
|
document.getElementById('client-count').textContent = '';
|
|
679
758
|
return;
|
|
680
759
|
}
|
|
681
760
|
|
|
682
761
|
document.getElementById('client-count').textContent = '(' + clients.length + ' connected)';
|
|
683
|
-
|
|
762
|
+
const fragment = document.createDocumentFragment();
|
|
684
763
|
clients.forEach((c, idx) => {
|
|
764
|
+
const card = document.createElement('div');
|
|
765
|
+
card.className = 'client-card';
|
|
685
766
|
if (c.error) {
|
|
686
|
-
|
|
767
|
+
const title = document.createElement('strong');
|
|
768
|
+
title.textContent = 'Client #' + (idx + 1);
|
|
769
|
+
card.append(title, document.createTextNode(' — payload decryption failed'));
|
|
770
|
+
fragment.appendChild(card);
|
|
687
771
|
return;
|
|
688
772
|
}
|
|
689
773
|
const when = c.time || (c.seenAt ? new Date(c.seenAt).toLocaleTimeString() : 'Unknown');
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
774
|
+
const title = document.createElement('strong');
|
|
775
|
+
title.textContent = String(c.username || 'Unknown');
|
|
776
|
+
card.append(title, document.createTextNode(' — ' + String(c.action || 'connected')), document.createElement('br'));
|
|
777
|
+
for (const field of [
|
|
778
|
+
['IP', c.ip],
|
|
779
|
+
['OS', c.os],
|
|
780
|
+
['CPU', c.cpu],
|
|
781
|
+
['RAM', c.ram],
|
|
782
|
+
['Time', when]
|
|
783
|
+
]) {
|
|
784
|
+
const meta = document.createElement('span');
|
|
785
|
+
meta.className = 'meta';
|
|
786
|
+
meta.textContent = field[0] + ': ' + String(field[1] || 'Unknown');
|
|
787
|
+
card.appendChild(meta);
|
|
788
|
+
if (field[0] !== 'Time') card.appendChild(document.createElement('br'));
|
|
789
|
+
}
|
|
790
|
+
fragment.appendChild(card);
|
|
699
791
|
});
|
|
700
|
-
container.
|
|
792
|
+
container.replaceChildren(fragment);
|
|
701
793
|
}
|
|
702
794
|
|
|
703
795
|
</script>
|
|
@@ -709,7 +801,7 @@ function renderClients(clients) {
|
|
|
709
801
|
const port = server.address().port;
|
|
710
802
|
const url = `http://127.0.0.1:${port}`;
|
|
711
803
|
console.log(chalk.green(` ✓ Local dashboard: ${url}`));
|
|
712
|
-
try { await
|
|
804
|
+
try { await openUrl(url); } catch { }
|
|
713
805
|
resolve({ url, close: () => server.close() });
|
|
714
806
|
});
|
|
715
807
|
});
|
|
@@ -721,7 +813,6 @@ function renderClients(clients) {
|
|
|
721
813
|
async function spawnPrivateBroker() {
|
|
722
814
|
console.log(chalk.yellow('\n ⚠️ Public Broker is unreachable. Spawning Private Broker...'));
|
|
723
815
|
|
|
724
|
-
const packageRoot = path.resolve(__dirname, '../..');
|
|
725
816
|
const serverEntrypoint = path.join(__dirname, '../server.js');
|
|
726
817
|
const requireFromServer = createRequire(serverEntrypoint);
|
|
727
818
|
const requiredBrokerPackages = ['express', 'express-rate-limit', 'helmet'];
|
|
@@ -735,34 +826,10 @@ async function spawnPrivateBroker() {
|
|
|
735
826
|
});
|
|
736
827
|
|
|
737
828
|
if (missingPackages.length > 0) {
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
npmCmd,
|
|
742
|
-
['install', '--no-save', '--no-audit', '--no-fund', ...missingPackages],
|
|
743
|
-
{ cwd: packageRoot, reject: false, all: true }
|
|
829
|
+
throw new Error(
|
|
830
|
+
`Private broker dependencies are missing: ${missingPackages.join(', ')}. `
|
|
831
|
+
+ 'Reinstall iPingYou from its verified package before starting a private broker.'
|
|
744
832
|
);
|
|
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
833
|
}
|
|
767
834
|
|
|
768
835
|
// 1. Spawn the broker server process
|
|
@@ -823,6 +890,7 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
823
890
|
let dashboardInstance = null;
|
|
824
891
|
|
|
825
892
|
const renderDashboard = () => {
|
|
893
|
+
const isPrivateBroker = Boolean(global.privateBrokerInstance);
|
|
826
894
|
console.clear();
|
|
827
895
|
console.log('');
|
|
828
896
|
console.log(chalk.bold(' ╔════════════════════════════════════════════════════╗'));
|
|
@@ -831,7 +899,9 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
831
899
|
console.log(` ║ ${chalk.cyan('UID:')} ${chalk.bold.white(uid.padEnd(30))}║`);
|
|
832
900
|
console.log(` ║ ${chalk.cyan('Password:')} ${chalk.bold.white(secureSensitive(password).padEnd(30))}║`);
|
|
833
901
|
console.log(` ║ ${chalk.cyan('Service:')} ${chalk.dim(serviceConfig.type.toUpperCase() + ' (Port ' + serviceConfig.port + ')').padEnd(30)}║`);
|
|
834
|
-
|
|
902
|
+
if (isPrivateBroker) {
|
|
903
|
+
console.log(` ║ ${chalk.cyan('Tunnel:')} ${chalk.dim(sessionState.tunnelUrl.substring(0, 40))} ║`);
|
|
904
|
+
}
|
|
835
905
|
if (serviceConfig.chatUrl) {
|
|
836
906
|
console.log(` ║ ${chalk.cyan('Chat URL:')} ${chalk.dim(serviceConfig.chatUrl.substring(0, 40))} ║`);
|
|
837
907
|
}
|
|
@@ -1080,12 +1150,11 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
1080
1150
|
return waitForAction();
|
|
1081
1151
|
|
|
1082
1152
|
case 'terminate': {
|
|
1083
|
-
const spinner = createSpinner('
|
|
1153
|
+
const spinner = createSpinner('Revoking session and closing its tunnel...', networkSpinner).start();
|
|
1084
1154
|
try {
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
await execa('pkill', ['-f', 'sshd:.*@'], { reject: false });
|
|
1155
|
+
await revokeUID(BROKER_URL, uid, sessionState.hostToken);
|
|
1156
|
+
tunnelProcess.kill();
|
|
1157
|
+
if (process.platform !== 'win32') {
|
|
1089
1158
|
await execa('tmux', [...tmuxSocketArgs(), 'kill-server'], { reject: false });
|
|
1090
1159
|
const legacySessions = await listTmuxSessions();
|
|
1091
1160
|
for (const session of legacySessions) {
|
|
@@ -1094,7 +1163,7 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
1094
1163
|
}
|
|
1095
1164
|
}
|
|
1096
1165
|
}
|
|
1097
|
-
spinner.succeed('
|
|
1166
|
+
spinner.succeed('Session revoked and iPingYou-owned connections terminated');
|
|
1098
1167
|
logSessionEvent('host_sessions_terminated');
|
|
1099
1168
|
} catch {
|
|
1100
1169
|
spinner.warn('Could not terminate sessions (none active?)');
|
|
@@ -1271,13 +1340,13 @@ export async function startHostMode() {
|
|
|
1271
1340
|
console.log(chalk.dim(' 🔑 Generating ephemeral SSH key for passwordless entry...'));
|
|
1272
1341
|
try {
|
|
1273
1342
|
const ephemeralKey = await generateEphemeralKey();
|
|
1274
|
-
const authKeysPath = await injectPublicKey(ephemeralKey.pubKey);
|
|
1343
|
+
const { authKeysPath, authorizedKey } = await injectPublicKey(ephemeralKey.pubKey);
|
|
1275
1344
|
|
|
1276
1345
|
serviceConfig.privateKey = ephemeralKey.privKey;
|
|
1277
1346
|
|
|
1278
1347
|
addCleanupHook(async () => {
|
|
1279
1348
|
console.log(chalk.dim(' Removing ephemeral public key...'));
|
|
1280
|
-
await removePublicKey(authKeysPath,
|
|
1349
|
+
await removePublicKey(authKeysPath, authorizedKey);
|
|
1281
1350
|
try { await fs.promises.unlink(ephemeralKey.keyPath); } catch { }
|
|
1282
1351
|
try { await fs.promises.unlink(`${ephemeralKey.keyPath}.pub`); } catch { }
|
|
1283
1352
|
});
|
package/src/server.js
CHANGED
|
@@ -435,8 +435,11 @@ app.get('/approval-requests/:uid', hostLimiter, (req, res) => {
|
|
|
435
435
|
if (!entry) return res.status(404).json({ error: 'UID not found' });
|
|
436
436
|
// Require host token to view approval list
|
|
437
437
|
if (!requireHostToken(req, res, entry)) return;
|
|
438
|
-
//
|
|
439
|
-
const sanitized = entry.approvals.map(a => ({
|
|
438
|
+
// Include encrypted payloads so the host can decrypt client details locally
|
|
439
|
+
const sanitized = entry.approvals.map(a => ({
|
|
440
|
+
id: a.id, status: a.status, createdAt: a.createdAt, decidedAt: a.decidedAt,
|
|
441
|
+
iv: a.iv, ciphertext: a.ciphertext, salt: a.salt,
|
|
442
|
+
}));
|
|
440
443
|
res.json({ approvalRequired: entry.approvalRequired, approvals: sanitized });
|
|
441
444
|
});
|
|
442
445
|
|