@miraj181/ipingyou 2.1.18 → 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} +25 -9
- 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 +99 -25
- package/src/modes/doctor.js +5 -7
- package/src/modes/host.js +326 -154
- package/src/server.js +55 -6
- 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/src/modes/host.js
CHANGED
|
@@ -22,18 +22,17 @@ import { createRequire } from 'node:module';
|
|
|
22
22
|
import fs from 'node:fs';
|
|
23
23
|
import os from 'node:os';
|
|
24
24
|
import crypto from 'node:crypto';
|
|
25
|
-
import { generateUID } from '../lib/uid.js';
|
|
26
|
-
import { openUrl } from '../lib/open-url.js';
|
|
27
|
-
import { decryptAsync } from '../lib/crypto.js';
|
|
28
|
-
import { cleanupAll, killProcessTree, trackPID, untrackPID, setRevokeOnExit, addCleanupHook } from '../lib/cleanup.js';
|
|
29
|
-
import { detectOS } from '../lib/platform.js';
|
|
30
|
-
import { createSpinner, networkSpinner, typeText } from '../lib/animations.js';
|
|
31
|
-
import { startChatServer, openLocalChatUI } from '../lib/chat.js';
|
|
32
|
-
import { secureSensitive } from '../lib/secure-print.js';
|
|
33
|
-
import { spawnTunnelSupervised } from '../lib/tunnel.js';
|
|
34
|
-
import { decideApprovalRequest, fetchApprovalRequests, pingBroker, registerWithBroker, revokeUID } from '../lib/broker.js';
|
|
35
|
-
import { cleanupSessionLog, getSessionLogPath, initSessionLog, logSessionEvent, recordEvent } from '../lib/session-log.js';
|
|
36
|
-
import { TMUX_SESSION_NAME, TMUX_SESSION_PREFIX, tmuxSocketArgs } from '../lib/tmux.js';
|
|
25
|
+
import { generateUID } from '../lib/mod/uid.js';
|
|
26
|
+
import { openUrl } from '../lib/mod/open-url.js';
|
|
27
|
+
import { decryptAsync, encryptAsync } from '../lib/mod/crypto.js';
|
|
28
|
+
import { cleanupAll, killProcessTree, trackPID, untrackPID, setRevokeOnExit, addCleanupHook } from '../lib/mod/cleanup.js';
|
|
29
|
+
import { detectOS, isLinuxSSHActive, startLinuxSSH } from '../lib/services/platform.js';
|
|
30
|
+
import { createSpinner, networkSpinner, typeText } from '../lib/mod/animations.js';
|
|
31
|
+
import { startChatServer, openLocalChatUI } from '../lib/services/chat.js';
|
|
32
|
+
import { secureSensitive } from '../lib/mod/secure-print.js';
|
|
33
|
+
import { spawnTunnelSupervised } from '../lib/services/tunnel.js';
|
|
34
|
+
import { decideApprovalRequest, fetchApprovalRequests, pingBroker, registerWithBroker, revokeUID } from '../lib/client/broker.js';
|
|
35
|
+
import { cleanupSessionLog, getSessionLogPath, initSessionLog, logSessionEvent, recordEvent } from '../lib/mod/session-log.js';
|
|
37
36
|
|
|
38
37
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
39
38
|
let BROKER_URL = process.env.BROKER_URL || 'https://ipingyou.onrender.com';
|
|
@@ -68,18 +67,13 @@ async function ensureSSHRunning() {
|
|
|
68
67
|
|
|
69
68
|
try {
|
|
70
69
|
if (osInfo.isLinux) {
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
const active = await isLinuxSSHActive();
|
|
71
|
+
if (active) {
|
|
73
72
|
spinner.succeed('SSH service is active');
|
|
74
|
-
}
|
|
73
|
+
} else {
|
|
75
74
|
spinner.text = 'Starting SSH service...';
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
spinner.succeed('SSH service started');
|
|
79
|
-
} catch {
|
|
80
|
-
await execa('sudo', ['systemctl', 'start', 'sshd'], { stdio: 'inherit' });
|
|
81
|
-
spinner.succeed('SSH service started (sshd)');
|
|
82
|
-
}
|
|
75
|
+
await startLinuxSSH();
|
|
76
|
+
spinner.succeed('SSH service started');
|
|
83
77
|
}
|
|
84
78
|
} else if (osInfo.isMac) {
|
|
85
79
|
try {
|
|
@@ -116,82 +110,191 @@ async function ensureSSHRunning() {
|
|
|
116
110
|
}
|
|
117
111
|
}
|
|
118
112
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
113
|
+
function formatAndPrintLogLine(line) {
|
|
114
|
+
try {
|
|
115
|
+
const data = JSON.parse(line);
|
|
116
|
+
const time = new Date(data.timestamp).toLocaleTimeString();
|
|
117
|
+
const typeLabel = chalk.bold(data.type);
|
|
118
|
+
|
|
119
|
+
let color = chalk.white;
|
|
120
|
+
let icon = 'ℹ️';
|
|
121
|
+
if (data.level === 'warn') {
|
|
122
|
+
color = chalk.yellow;
|
|
123
|
+
icon = '⚠️';
|
|
124
|
+
} else if (data.level === 'error') {
|
|
125
|
+
color = chalk.red;
|
|
126
|
+
icon = '❌';
|
|
127
|
+
} else if (data.type.includes('success') || data.type.includes('complete') || data.type.includes('granted') || data.type.includes('start')) {
|
|
128
|
+
color = chalk.green;
|
|
129
|
+
icon = '✓';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const detailsStr = Object.keys(data.details || {}).length > 0
|
|
133
|
+
? chalk.dim(JSON.stringify(data.details))
|
|
134
|
+
: '';
|
|
135
|
+
|
|
136
|
+
console.log(` [${chalk.dim(time)}] ${color(icon)} ${color(typeLabel)} ${detailsStr}`);
|
|
137
|
+
} catch {
|
|
138
|
+
if (line.trim()) {
|
|
139
|
+
console.log(` ${chalk.dim(line)}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function viewLiveClientLogs(sharedDropPath) {
|
|
145
|
+
if (!sharedDropPath) {
|
|
146
|
+
console.log(chalk.red(' ❌ Error: Shared drop path is not configured.'));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
125
149
|
|
|
126
|
-
const spinner = createSpinner('Checking tmux installation...', networkSpinner).start();
|
|
127
150
|
try {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
151
|
+
const files = await fs.promises.readdir(sharedDropPath);
|
|
152
|
+
const clientLogs = files.filter(f => f.startsWith('client-') && f.endsWith('.log'));
|
|
153
|
+
|
|
154
|
+
if (clientLogs.length === 0) {
|
|
155
|
+
console.log(chalk.yellow('\n No active client logs found in the shared drop folder.'));
|
|
156
|
+
console.log(chalk.dim(' Clients automatically share their logs once connected.'));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const { selectedLog } = await inquirer.prompt([
|
|
161
|
+
{
|
|
162
|
+
type: 'list',
|
|
163
|
+
name: 'selectedLog',
|
|
164
|
+
message: 'Select a client to monitor activity in live:',
|
|
165
|
+
choices: clientLogs.map(f => {
|
|
166
|
+
const clientName = f.replace('client-', '').replace('.log', '');
|
|
167
|
+
return { name: `👤 ${clientName}`, value: f };
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
]);
|
|
171
|
+
|
|
172
|
+
const logFilePath = path.join(sharedDropPath, selectedLog);
|
|
173
|
+
const clientName = selectedLog.replace('client-', '').replace('.log', '');
|
|
174
|
+
|
|
175
|
+
console.log('');
|
|
176
|
+
console.log(chalk.bold.cyan(` 📊 Live Monitor: ${clientName}`));
|
|
177
|
+
console.log(chalk.dim(' ──────────────────────────────────────────────────'));
|
|
178
|
+
console.log(chalk.dim(' Showing log stream. Press Enter to exit.'));
|
|
179
|
+
console.log('');
|
|
180
|
+
|
|
181
|
+
let filePosition = 0;
|
|
182
|
+
let keepWatching = true;
|
|
183
|
+
|
|
184
|
+
if (fs.existsSync(logFilePath)) {
|
|
185
|
+
const stats = fs.statSync(logFilePath);
|
|
186
|
+
filePosition = stats.size;
|
|
187
|
+
const content = fs.readFileSync(logFilePath, 'utf8');
|
|
188
|
+
const lines = content.split('\n').filter(Boolean);
|
|
189
|
+
const lastLines = lines.slice(-10);
|
|
190
|
+
for (const line of lastLines) {
|
|
191
|
+
formatAndPrintLogLine(line);
|
|
156
192
|
}
|
|
157
193
|
}
|
|
194
|
+
|
|
195
|
+
const intervalId = setInterval(() => {
|
|
196
|
+
if (!keepWatching) return;
|
|
197
|
+
try {
|
|
198
|
+
if (!fs.existsSync(logFilePath)) return;
|
|
199
|
+
const stats = fs.statSync(logFilePath);
|
|
200
|
+
if (stats.size > filePosition) {
|
|
201
|
+
const fd = fs.openSync(logFilePath, 'r');
|
|
202
|
+
const bufferSize = stats.size - filePosition;
|
|
203
|
+
const buffer = Buffer.allocUnsafe(bufferSize);
|
|
204
|
+
fs.readSync(fd, buffer, 0, bufferSize, filePosition);
|
|
205
|
+
fs.closeSync(fd);
|
|
206
|
+
|
|
207
|
+
filePosition = stats.size;
|
|
208
|
+
const newContent = buffer.toString('utf8');
|
|
209
|
+
const lines = newContent.split('\n').filter(Boolean);
|
|
210
|
+
for (const line of lines) {
|
|
211
|
+
formatAndPrintLogLine(line);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
} catch {
|
|
215
|
+
// Ignore file access errors
|
|
216
|
+
}
|
|
217
|
+
}, 1000);
|
|
218
|
+
|
|
219
|
+
await inquirer.prompt([{
|
|
220
|
+
type: 'input',
|
|
221
|
+
name: 'exit',
|
|
222
|
+
message: 'Press Enter to stop monitoring...'
|
|
223
|
+
}]);
|
|
224
|
+
|
|
225
|
+
keepWatching = false;
|
|
226
|
+
clearInterval(intervalId);
|
|
227
|
+
console.log(chalk.cyan(' Stopped monitoring client logs.'));
|
|
158
228
|
} catch (err) {
|
|
159
|
-
|
|
160
|
-
console.log(chalk.dim(' Terminal Mirroring feature will not be available.'));
|
|
229
|
+
console.log(chalk.red(` Could not read client logs: ${err.message}`));
|
|
161
230
|
}
|
|
162
231
|
}
|
|
163
232
|
|
|
164
|
-
function
|
|
165
|
-
|
|
166
|
-
|
|
233
|
+
async function viewHostLiveLogs() {
|
|
234
|
+
const logFilePath = getSessionLogPath();
|
|
235
|
+
if (!logFilePath || !fs.existsSync(logFilePath)) {
|
|
236
|
+
console.log(chalk.red(" ❌ Error: Host session log is not active or doesn't exist yet."));
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
167
239
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
.filter(Boolean)
|
|
174
|
-
.map(line => {
|
|
175
|
-
const [name, createdAt] = line.split('|');
|
|
176
|
-
return { name, createdAt: Number(createdAt) || null };
|
|
177
|
-
});
|
|
178
|
-
}
|
|
240
|
+
console.log('');
|
|
241
|
+
console.log(chalk.bold.cyan(` 📊 Live Monitor: Host Session Activity`));
|
|
242
|
+
console.log(chalk.dim(' ──────────────────────────────────────────────────'));
|
|
243
|
+
console.log(chalk.dim(' Showing log stream. Press Enter to exit.'));
|
|
244
|
+
console.log('');
|
|
179
245
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const customSessions = await listTmuxSessions(tmuxSocketArgs());
|
|
183
|
-
customSessions
|
|
184
|
-
.filter(s => isSecureLinkSession(s.name))
|
|
185
|
-
.forEach(s => sessions.push({ ...s, socketArgs: tmuxSocketArgs(), source: 'custom' }));
|
|
246
|
+
let filePosition = 0;
|
|
247
|
+
let keepWatching = true;
|
|
186
248
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
249
|
+
try {
|
|
250
|
+
const stats = fs.statSync(logFilePath);
|
|
251
|
+
filePosition = stats.size;
|
|
252
|
+
const content = fs.readFileSync(logFilePath, 'utf8');
|
|
253
|
+
const lines = content.split('\n').filter(Boolean);
|
|
254
|
+
const lastLines = lines.slice(-10);
|
|
255
|
+
for (const line of lastLines) {
|
|
256
|
+
formatAndPrintLogLine(line);
|
|
257
|
+
}
|
|
191
258
|
|
|
192
|
-
|
|
259
|
+
const intervalId = setInterval(() => {
|
|
260
|
+
if (!keepWatching) return;
|
|
261
|
+
try {
|
|
262
|
+
if (!fs.existsSync(logFilePath)) return;
|
|
263
|
+
const stats = fs.statSync(logFilePath);
|
|
264
|
+
if (stats.size > filePosition) {
|
|
265
|
+
const fd = fs.openSync(logFilePath, 'r');
|
|
266
|
+
const bufferSize = stats.size - filePosition;
|
|
267
|
+
const buffer = Buffer.allocUnsafe(bufferSize);
|
|
268
|
+
fs.readSync(fd, buffer, 0, bufferSize, filePosition);
|
|
269
|
+
fs.closeSync(fd);
|
|
270
|
+
|
|
271
|
+
filePosition = stats.size;
|
|
272
|
+
const newContent = buffer.toString('utf8');
|
|
273
|
+
const lines = newContent.split('\n').filter(Boolean);
|
|
274
|
+
for (const line of lines) {
|
|
275
|
+
formatAndPrintLogLine(line);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
} catch {
|
|
279
|
+
// Ignore file access errors
|
|
280
|
+
}
|
|
281
|
+
}, 1000);
|
|
282
|
+
|
|
283
|
+
await inquirer.prompt([{
|
|
284
|
+
type: 'input',
|
|
285
|
+
name: 'exit',
|
|
286
|
+
message: 'Press Enter to stop monitoring...'
|
|
287
|
+
}]);
|
|
288
|
+
|
|
289
|
+
keepWatching = false;
|
|
290
|
+
clearInterval(intervalId);
|
|
291
|
+
console.log(chalk.cyan(' Stopped monitoring host logs.'));
|
|
292
|
+
} catch (err) {
|
|
293
|
+
console.log(chalk.red(` Could not read host logs: ${err.message}`));
|
|
294
|
+
}
|
|
193
295
|
}
|
|
194
296
|
|
|
297
|
+
|
|
195
298
|
// ─── Ephemeral SSH Key Management ────────────────────────────
|
|
196
299
|
async function generateEphemeralKey() {
|
|
197
300
|
const tmpDir = os.tmpdir() || process.env.TMPDIR || process.env.TEMP || process.env.TMP;
|
|
@@ -219,6 +322,9 @@ async function injectPublicKey(pubKey) {
|
|
|
219
322
|
if (!fs.existsSync(sshDir)) {
|
|
220
323
|
await fs.promises.mkdir(sshDir, { mode: 0o700, recursive: true });
|
|
221
324
|
}
|
|
325
|
+
try {
|
|
326
|
+
await fs.promises.chmod(sshDir, 0o700);
|
|
327
|
+
} catch {}
|
|
222
328
|
|
|
223
329
|
const authKeysPath = path.join(sshDir, 'authorized_keys');
|
|
224
330
|
const existing = await fs.promises.lstat(authKeysPath).catch(() => null);
|
|
@@ -227,20 +333,47 @@ async function injectPublicKey(pubKey) {
|
|
|
227
333
|
}
|
|
228
334
|
const authorizedKey = `no-agent-forwarding,no-X11-forwarding ${pubKey}`;
|
|
229
335
|
await fs.promises.appendFile(authKeysPath, `\n${authorizedKey}\n`, { mode: 0o600 });
|
|
230
|
-
|
|
231
|
-
|
|
336
|
+
try {
|
|
337
|
+
await fs.promises.chmod(authKeysPath, 0o600);
|
|
338
|
+
} catch {}
|
|
339
|
+
|
|
340
|
+
// Windows Administrators authorized keys handling
|
|
341
|
+
let adminAuthKeysPath = null;
|
|
342
|
+
if (process.platform === 'win32') {
|
|
343
|
+
const programData = process.env.PROGRAMDATA || 'C:\\ProgramData';
|
|
344
|
+
const adminKeysPath = path.join(programData, 'ssh', 'administrators_authorized_keys');
|
|
345
|
+
try {
|
|
346
|
+
if (fs.existsSync(path.dirname(adminKeysPath))) {
|
|
347
|
+
await fs.promises.appendFile(adminKeysPath, `\n${authorizedKey}\n`, { mode: 0o600 });
|
|
348
|
+
try {
|
|
349
|
+
await execa('icacls', [adminKeysPath, '/inheritance:r', '/grant', '*S-1-5-32-544:F', '/grant', '*S-1-5-18:F']);
|
|
350
|
+
} catch {}
|
|
351
|
+
adminAuthKeysPath = adminKeysPath;
|
|
352
|
+
}
|
|
353
|
+
} catch {}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return { authKeysPath, adminAuthKeysPath, authorizedKey };
|
|
232
357
|
}
|
|
233
358
|
|
|
234
|
-
async function removePublicKey(authKeysPath, authorizedKey) {
|
|
359
|
+
async function removePublicKey(authKeysPath, authorizedKey, adminAuthKeysPath = null) {
|
|
235
360
|
if (fs.existsSync(authKeysPath)) {
|
|
236
361
|
const stat = await fs.promises.lstat(authKeysPath);
|
|
237
|
-
if (stat.isSymbolicLink()) {
|
|
238
|
-
|
|
362
|
+
if (!stat.isSymbolicLink()) {
|
|
363
|
+
let keys = await fs.promises.readFile(authKeysPath, 'utf8');
|
|
364
|
+
keys = keys.replace(`\n${authorizedKey}\n`, '');
|
|
365
|
+
await fs.promises.writeFile(authKeysPath, keys);
|
|
366
|
+
try {
|
|
367
|
+
await fs.promises.chmod(authKeysPath, 0o600);
|
|
368
|
+
} catch {}
|
|
239
369
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
370
|
+
}
|
|
371
|
+
if (adminAuthKeysPath && fs.existsSync(adminAuthKeysPath)) {
|
|
372
|
+
try {
|
|
373
|
+
let keys = await fs.promises.readFile(adminAuthKeysPath, 'utf8');
|
|
374
|
+
keys = keys.replace(`\n${authorizedKey}\n`, '');
|
|
375
|
+
await fs.promises.writeFile(adminAuthKeysPath, keys);
|
|
376
|
+
} catch {}
|
|
244
377
|
}
|
|
245
378
|
}
|
|
246
379
|
|
|
@@ -344,6 +477,29 @@ async function startLocalHostDashboard(uid, password, serviceConfig, sessionStat
|
|
|
344
477
|
return { clients: decryptedClients };
|
|
345
478
|
}
|
|
346
479
|
|
|
480
|
+
async function fetchDecryptedApprovals() {
|
|
481
|
+
const data = await fetchApprovalRequests(BROKER_URL, uid, sessionState.hostToken);
|
|
482
|
+
const decryptedApprovals = await Promise.all((data.approvals || []).map(async (a) => {
|
|
483
|
+
const base = { id: a.id, status: a.status, createdAt: a.createdAt, decidedAt: a.decidedAt, ip: a.ip || 'unknown' };
|
|
484
|
+
if (!a.iv || !a.ciphertext || !a.salt) return base;
|
|
485
|
+
try {
|
|
486
|
+
const decrypted = await decryptAsync(a.iv, a.ciphertext, password, a.salt);
|
|
487
|
+
const details = JSON.parse(decrypted);
|
|
488
|
+
return {
|
|
489
|
+
...base,
|
|
490
|
+
username: details.username,
|
|
491
|
+
hostname: details.hostname,
|
|
492
|
+
os: details.os,
|
|
493
|
+
intent: details.intent,
|
|
494
|
+
localIp: details.localIp || 'unknown'
|
|
495
|
+
};
|
|
496
|
+
} catch {
|
|
497
|
+
return base;
|
|
498
|
+
}
|
|
499
|
+
}));
|
|
500
|
+
return { approvalRequired: data.approvalRequired, approvals: decryptedApprovals };
|
|
501
|
+
}
|
|
502
|
+
|
|
347
503
|
app.get('/api/status', (_req, res) => {
|
|
348
504
|
res.json({
|
|
349
505
|
uid,
|
|
@@ -361,7 +517,7 @@ async function startLocalHostDashboard(uid, password, serviceConfig, sessionStat
|
|
|
361
517
|
|
|
362
518
|
app.get('/api/approvals', async (_req, res) => {
|
|
363
519
|
try {
|
|
364
|
-
const data = await
|
|
520
|
+
const data = await fetchDecryptedApprovals();
|
|
365
521
|
res.json(data);
|
|
366
522
|
} catch (err) {
|
|
367
523
|
res.status(500).json({ error: err.message });
|
|
@@ -399,7 +555,7 @@ async function startLocalHostDashboard(uid, password, serviceConfig, sessionStat
|
|
|
399
555
|
if (closed) return;
|
|
400
556
|
try {
|
|
401
557
|
const [approvalData, clientData] = await Promise.all([
|
|
402
|
-
|
|
558
|
+
fetchDecryptedApprovals().catch(() => ({ approvals: [] })),
|
|
403
559
|
fetchDecryptedClients().catch(() => ({ clients: [] })),
|
|
404
560
|
]);
|
|
405
561
|
|
|
@@ -472,7 +628,31 @@ async function startLocalHostDashboard(uid, password, serviceConfig, sessionStat
|
|
|
472
628
|
const { requestId, decision } = req.body || {};
|
|
473
629
|
if (!requestId || !decision) return res.status(400).json({ error: 'requestId and decision required' });
|
|
474
630
|
try {
|
|
475
|
-
|
|
631
|
+
let approvedPayload = null;
|
|
632
|
+
if (decision === 'approved') {
|
|
633
|
+
const data = await fetchApprovalRequests(BROKER_URL, uid, sessionState.hostToken);
|
|
634
|
+
const request = (data.approvals || []).find(item => item.id === requestId);
|
|
635
|
+
if (!request) return res.status(404).json({ error: 'Request not found' });
|
|
636
|
+
|
|
637
|
+
let details = {};
|
|
638
|
+
try {
|
|
639
|
+
details = JSON.parse(await decryptAsync(request.iv, request.ciphertext, password, request.salt));
|
|
640
|
+
} catch {}
|
|
641
|
+
|
|
642
|
+
const clientKeySalt = [
|
|
643
|
+
password,
|
|
644
|
+
request.ip || 'unknown',
|
|
645
|
+
details.username || 'unknown',
|
|
646
|
+
details.hostname || 'unknown',
|
|
647
|
+
details.os || 'unknown'
|
|
648
|
+
].join('|');
|
|
649
|
+
const clientPwd = crypto.createHash('sha256').update(clientKeySalt).digest('hex');
|
|
650
|
+
|
|
651
|
+
const payload = JSON.stringify({ url: sessionState.tunnelUrl, ...serviceConfig });
|
|
652
|
+
approvedPayload = await encryptAsync(payload, clientPwd);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
await decideApprovalRequest(BROKER_URL, uid, requestId, decision, sessionState.hostToken, approvedPayload);
|
|
476
656
|
recordEvent('approval_decision', { uid, requestId, decision, via: 'dashboard' });
|
|
477
657
|
res.json({ ok: true });
|
|
478
658
|
} catch (err) {
|
|
@@ -666,6 +846,9 @@ function renderApprovals(approvals) {
|
|
|
666
846
|
badge.className = 'status-badge status-pending';
|
|
667
847
|
badge.textContent = 'PENDING';
|
|
668
848
|
heading.append(title, badge);
|
|
849
|
+
const details = document.createElement('div');
|
|
850
|
+
details.className = 'meta';
|
|
851
|
+
details.textContent = 'User: ' + String(req.username || 'unknown') + ' | Host: ' + String(req.hostname || 'unknown') + ' | OS: ' + String(req.os || 'unknown') + ' | IP: ' + String(req.ip || 'unknown') + ' (Local: ' + String(req.localIp || 'unknown') + ')';
|
|
669
852
|
const meta = document.createElement('div');
|
|
670
853
|
meta.className = 'meta';
|
|
671
854
|
meta.textContent = 'Submitted: ' + new Date(req.createdAt).toLocaleTimeString();
|
|
@@ -684,7 +867,7 @@ function renderApprovals(approvals) {
|
|
|
684
867
|
});
|
|
685
868
|
actions.appendChild(button);
|
|
686
869
|
}
|
|
687
|
-
item.append(heading, meta, actions);
|
|
870
|
+
item.append(heading, details, meta, actions);
|
|
688
871
|
fragment.appendChild(item);
|
|
689
872
|
}
|
|
690
873
|
for (const req of decided) {
|
|
@@ -871,6 +1054,7 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
871
1054
|
let dashboardInstance = null;
|
|
872
1055
|
|
|
873
1056
|
const renderDashboard = () => {
|
|
1057
|
+
const isPrivateBroker = Boolean(global.privateBrokerInstance);
|
|
874
1058
|
console.clear();
|
|
875
1059
|
console.log('');
|
|
876
1060
|
console.log(chalk.bold(' ╔════════════════════════════════════════════════════╗'));
|
|
@@ -879,7 +1063,9 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
879
1063
|
console.log(` ║ ${chalk.cyan('UID:')} ${chalk.bold.white(uid.padEnd(30))}║`);
|
|
880
1064
|
console.log(` ║ ${chalk.cyan('Password:')} ${chalk.bold.white(secureSensitive(password).padEnd(30))}║`);
|
|
881
1065
|
console.log(` ║ ${chalk.cyan('Service:')} ${chalk.dim(serviceConfig.type.toUpperCase() + ' (Port ' + serviceConfig.port + ')').padEnd(30)}║`);
|
|
882
|
-
|
|
1066
|
+
if (isPrivateBroker) {
|
|
1067
|
+
console.log(` ║ ${chalk.cyan('Tunnel:')} ${chalk.dim(sessionState.tunnelUrl.substring(0, 40))} ║`);
|
|
1068
|
+
}
|
|
883
1069
|
if (serviceConfig.chatUrl) {
|
|
884
1070
|
console.log(` ║ ${chalk.cyan('Chat URL:')} ${chalk.dim(serviceConfig.chatUrl.substring(0, 40))} ║`);
|
|
885
1071
|
}
|
|
@@ -910,7 +1096,8 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
910
1096
|
const choices = [
|
|
911
1097
|
{ name: '✅ Review pending client approvals', value: 'approvals' },
|
|
912
1098
|
{ name: '📡 See detailed client telemetry', value: 'show' },
|
|
913
|
-
{ name: '
|
|
1099
|
+
{ name: '📄 View live client activity logs', value: 'logs' },
|
|
1100
|
+
{ name: '📄 View host session activity logs', value: 'host_logs' },
|
|
914
1101
|
{ name: '🔄 Re-register with broker', value: 'reregister' }
|
|
915
1102
|
];
|
|
916
1103
|
|
|
@@ -964,6 +1151,7 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
964
1151
|
console.log(` Host: ${details.hostname || 'unknown'}`);
|
|
965
1152
|
console.log(` OS: ${details.os || 'unknown'}`);
|
|
966
1153
|
console.log(` Intent: ${details.intent || 'connect'}`);
|
|
1154
|
+
console.log(` IP: ${request.ip || 'unknown'} (Local: ${details.localIp || 'unknown'})`);
|
|
967
1155
|
|
|
968
1156
|
const { decision } = await inquirer.prompt([{
|
|
969
1157
|
type: 'list',
|
|
@@ -976,7 +1164,22 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
976
1164
|
],
|
|
977
1165
|
}]);
|
|
978
1166
|
if (decision !== 'skip') {
|
|
979
|
-
|
|
1167
|
+
let approvedPayload = null;
|
|
1168
|
+
if (decision === 'approved') {
|
|
1169
|
+
const clientKeySalt = [
|
|
1170
|
+
password,
|
|
1171
|
+
request.ip || 'unknown',
|
|
1172
|
+
details.username || 'unknown',
|
|
1173
|
+
details.hostname || 'unknown',
|
|
1174
|
+
details.os || 'unknown'
|
|
1175
|
+
].join('|');
|
|
1176
|
+
const clientPwd = crypto.createHash('sha256').update(clientKeySalt).digest('hex');
|
|
1177
|
+
|
|
1178
|
+
const payload = JSON.stringify({ url: sessionState.tunnelUrl, ...serviceConfig });
|
|
1179
|
+
approvedPayload = await encryptAsync(payload, clientPwd);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
await decideApprovalRequest(BROKER_URL, uid, request.id, decision, sessionState.hostToken, approvedPayload);
|
|
980
1183
|
recordEvent('approval_decision', { uid, requestId: request.id, decision, username: details.username });
|
|
981
1184
|
}
|
|
982
1185
|
}
|
|
@@ -1001,6 +1204,14 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
1001
1204
|
}
|
|
1002
1205
|
});
|
|
1003
1206
|
|
|
1207
|
+
addCleanupHook(() => {
|
|
1208
|
+
try {
|
|
1209
|
+
if (chatServerInstance && chatServerInstance.server) {
|
|
1210
|
+
chatServerInstance.server.close();
|
|
1211
|
+
}
|
|
1212
|
+
} catch {}
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1004
1215
|
console.log(chalk.dim(' Provisioning Cloudflare tunnel for chat...'));
|
|
1005
1216
|
chatTunnelProcess = await spawnTunnelSupervised(`http://localhost:${chatServerInstance.port}`, async (newUrl) => {
|
|
1006
1217
|
serviceConfig.chatUrl = newUrl;
|
|
@@ -1019,6 +1230,9 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
1019
1230
|
|
|
1020
1231
|
case 'dashboard': {
|
|
1021
1232
|
dashboardInstance = await startLocalHostDashboard(uid, password, serviceConfig, sessionState);
|
|
1233
|
+
addCleanupHook(() => {
|
|
1234
|
+
try { if (dashboardInstance) dashboardInstance.close(); } catch {}
|
|
1235
|
+
});
|
|
1022
1236
|
logSessionEvent('host_dashboard_opened');
|
|
1023
1237
|
return waitForAction();
|
|
1024
1238
|
}
|
|
@@ -1035,48 +1249,12 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
1035
1249
|
return waitForAction();
|
|
1036
1250
|
}
|
|
1037
1251
|
|
|
1038
|
-
case '
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
console.log('');
|
|
1045
|
-
|
|
1046
|
-
try {
|
|
1047
|
-
await execa('tmux', ['-V'], { reject: true });
|
|
1048
|
-
const sessions = await getMirrorableSessions();
|
|
1049
|
-
if (sessions.length === 0) {
|
|
1050
|
-
console.log(chalk.yellow(' ⚠️ No mirrored terminal session is active yet.'));
|
|
1051
|
-
console.log(chalk.dim(' A client must choose "Connect via SSH" first. SCP-only clients do not create a tmux session.'));
|
|
1052
|
-
console.log(chalk.dim(' tmux is needed on the host machine only; the client does not need tmux.'));
|
|
1053
|
-
logSessionEvent('host_mirror_missing_session', {}, 'warn');
|
|
1054
|
-
return waitForAction();
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
let target = sessions[0];
|
|
1058
|
-
if (sessions.length > 1) {
|
|
1059
|
-
const { sessionChoice } = await inquirer.prompt([{
|
|
1060
|
-
type: 'list',
|
|
1061
|
-
name: 'sessionChoice',
|
|
1062
|
-
message: 'Select an active client session to mirror:',
|
|
1063
|
-
choices: sessions.map((s, idx) => {
|
|
1064
|
-
const created = s.createdAt ? new Date(s.createdAt * 1000).toLocaleTimeString() : 'Unknown';
|
|
1065
|
-
const label = `${s.name} ${chalk.dim(`(started ${created})`)}`;
|
|
1066
|
-
return { name: label, value: String(idx) };
|
|
1067
|
-
}),
|
|
1068
|
-
}]);
|
|
1069
|
-
target = sessions[parseInt(sessionChoice, 10)] || sessions[0];
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
await execa('tmux', [...target.socketArgs, 'attach', '-t', target.name, '-r'], { stdio: 'inherit', reject: false });
|
|
1073
|
-
logSessionEvent('host_mirror_attached', { session: target.name, source: target.source });
|
|
1074
|
-
} catch (err) {
|
|
1075
|
-
console.log(chalk.yellow(' ⚠️ Could not attach to tmux.'));
|
|
1076
|
-
console.log(chalk.dim(` ${err.message}`));
|
|
1077
|
-
console.log(chalk.dim(' Terminal mirroring requires tmux on the host machine and an active interactive SSH client.'));
|
|
1078
|
-
logSessionEvent('host_mirror_error', { error: err.message }, 'warn');
|
|
1079
|
-
}
|
|
1252
|
+
case 'logs': {
|
|
1253
|
+
await viewLiveClientLogs(serviceConfig.sharedDropPath);
|
|
1254
|
+
return waitForAction();
|
|
1255
|
+
}
|
|
1256
|
+
case 'host_logs': {
|
|
1257
|
+
await viewHostLiveLogs();
|
|
1080
1258
|
return waitForAction();
|
|
1081
1259
|
}
|
|
1082
1260
|
|
|
@@ -1132,15 +1310,7 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
1132
1310
|
try {
|
|
1133
1311
|
await revokeUID(BROKER_URL, uid, sessionState.hostToken);
|
|
1134
1312
|
tunnelProcess.kill();
|
|
1135
|
-
|
|
1136
|
-
await execa('tmux', [...tmuxSocketArgs(), 'kill-server'], { reject: false });
|
|
1137
|
-
const legacySessions = await listTmuxSessions();
|
|
1138
|
-
for (const session of legacySessions) {
|
|
1139
|
-
if (isSecureLinkSession(session.name)) {
|
|
1140
|
-
await execa('tmux', ['kill-session', '-t', session.name], { reject: false });
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1313
|
+
// No tmux server to terminate since mirroring was discarded
|
|
1144
1314
|
spinner.succeed('Session revoked and iPingYou-owned connections terminated');
|
|
1145
1315
|
logSessionEvent('host_sessions_terminated');
|
|
1146
1316
|
} catch {
|
|
@@ -1152,6 +1322,9 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
1152
1322
|
|
|
1153
1323
|
case 'exit':
|
|
1154
1324
|
if (dashboardInstance) dashboardInstance.close();
|
|
1325
|
+
if (chatServerInstance && chatServerInstance.server) {
|
|
1326
|
+
try { chatServerInstance.server.close(); } catch {}
|
|
1327
|
+
}
|
|
1155
1328
|
if (chatTunnelProcess) chatTunnelProcess.kill();
|
|
1156
1329
|
if (global.privateBrokerInstance) global.privateBrokerInstance.kill();
|
|
1157
1330
|
if (tunnelProcess) tunnelProcess.kill();
|
|
@@ -1288,7 +1461,6 @@ export async function startHostMode() {
|
|
|
1288
1461
|
|
|
1289
1462
|
if (serviceType === 'ssh' || serviceType === 'share') {
|
|
1290
1463
|
await ensureSSHRunning();
|
|
1291
|
-
await ensureTmuxInstalled();
|
|
1292
1464
|
|
|
1293
1465
|
try {
|
|
1294
1466
|
serviceConfig.sharedDropPath = await prepareSharedDropFolder(uid);
|
|
@@ -1318,13 +1490,13 @@ export async function startHostMode() {
|
|
|
1318
1490
|
console.log(chalk.dim(' 🔑 Generating ephemeral SSH key for passwordless entry...'));
|
|
1319
1491
|
try {
|
|
1320
1492
|
const ephemeralKey = await generateEphemeralKey();
|
|
1321
|
-
const { authKeysPath, authorizedKey } = await injectPublicKey(ephemeralKey.pubKey);
|
|
1493
|
+
const { authKeysPath, adminAuthKeysPath, authorizedKey } = await injectPublicKey(ephemeralKey.pubKey);
|
|
1322
1494
|
|
|
1323
1495
|
serviceConfig.privateKey = ephemeralKey.privKey;
|
|
1324
1496
|
|
|
1325
1497
|
addCleanupHook(async () => {
|
|
1326
1498
|
console.log(chalk.dim(' Removing ephemeral public key...'));
|
|
1327
|
-
await removePublicKey(authKeysPath, authorizedKey);
|
|
1499
|
+
await removePublicKey(authKeysPath, authorizedKey, adminAuthKeysPath);
|
|
1328
1500
|
try { await fs.promises.unlink(ephemeralKey.keyPath); } catch { }
|
|
1329
1501
|
try { await fs.promises.unlink(`${ephemeralKey.keyPath}.pub`); } catch { }
|
|
1330
1502
|
});
|