@miraj181/ipingyou 2.1.19 → 2.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -2
- package/package.json +1 -1
- package/src/cli.js +22 -14
- package/src/lib/ai/safety.js +1 -1
- package/src/lib/{broker.js → client/broker.js} +28 -12
- package/src/lib/{path-browser.js → client/path-browser.js} +6 -2
- package/src/lib/{session-log.js → mod/session-log.js} +17 -8
- package/src/lib/{worker-runtime.js → mod/worker-runtime.js} +1 -1
- package/src/lib/{chat.js → services/chat.js} +2 -2
- package/src/lib/services/platform.js +320 -0
- package/src/lib/{ssh.js → services/ssh.js} +14 -1
- package/src/lib/{tunnel.js → services/tunnel.js} +5 -3
- package/src/modes/ai.js +18 -9
- package/src/modes/client.js +103 -25
- package/src/modes/doctor.js +7 -8
- package/src/modes/host.js +322 -157
- package/src/server.js +50 -4
- package/src/lib/platform.js +0 -90
- /package/src/lib/{allowlist.js → mod/allowlist.js} +0 -0
- /package/src/lib/{animations.js → mod/animations.js} +0 -0
- /package/src/lib/{checksum.js → mod/checksum.js} +0 -0
- /package/src/lib/{cleanup.js → mod/cleanup.js} +0 -0
- /package/src/lib/{config.js → mod/config.js} +0 -0
- /package/src/lib/{crypto.js → mod/crypto.js} +0 -0
- /package/src/lib/{open-url.js → mod/open-url.js} +0 -0
- /package/src/lib/{secure-print.js → mod/secure-print.js} +0 -0
- /package/src/lib/{socket-firewall.js → mod/socket-firewall.js} +0 -0
- /package/src/lib/{tmux.js → mod/tmux.js} +0 -0
- /package/src/lib/{uid.js → mod/uid.js} +0 -0
package/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.time || 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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
246
|
+
let filePosition = 0;
|
|
247
|
+
let keepWatching = true;
|
|
248
|
+
|
|
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
|
+
}
|
|
258
|
+
|
|
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);
|
|
186
282
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
283
|
+
await inquirer.prompt([{
|
|
284
|
+
type: 'input',
|
|
285
|
+
name: 'exit',
|
|
286
|
+
message: 'Press Enter to stop monitoring...'
|
|
287
|
+
}]);
|
|
191
288
|
|
|
192
|
-
|
|
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;
|
|
@@ -214,11 +317,20 @@ async function injectPublicKey(pubKey) {
|
|
|
214
317
|
throw new Error('Could not resolve the current user home directory for authorized_keys');
|
|
215
318
|
}
|
|
216
319
|
|
|
320
|
+
if (process.platform !== 'win32') {
|
|
321
|
+
try {
|
|
322
|
+
await fs.promises.chmod(homedir, 0o755);
|
|
323
|
+
} catch {}
|
|
324
|
+
}
|
|
325
|
+
|
|
217
326
|
const sshDir = path.join(homedir, '.ssh');
|
|
218
327
|
|
|
219
328
|
if (!fs.existsSync(sshDir)) {
|
|
220
329
|
await fs.promises.mkdir(sshDir, { mode: 0o700, recursive: true });
|
|
221
330
|
}
|
|
331
|
+
try {
|
|
332
|
+
await fs.promises.chmod(sshDir, 0o700);
|
|
333
|
+
} catch {}
|
|
222
334
|
|
|
223
335
|
const authKeysPath = path.join(sshDir, 'authorized_keys');
|
|
224
336
|
const existing = await fs.promises.lstat(authKeysPath).catch(() => null);
|
|
@@ -227,20 +339,47 @@ async function injectPublicKey(pubKey) {
|
|
|
227
339
|
}
|
|
228
340
|
const authorizedKey = `no-agent-forwarding,no-X11-forwarding ${pubKey}`;
|
|
229
341
|
await fs.promises.appendFile(authKeysPath, `\n${authorizedKey}\n`, { mode: 0o600 });
|
|
230
|
-
|
|
231
|
-
|
|
342
|
+
try {
|
|
343
|
+
await fs.promises.chmod(authKeysPath, 0o600);
|
|
344
|
+
} catch {}
|
|
345
|
+
|
|
346
|
+
// Windows Administrators authorized keys handling
|
|
347
|
+
let adminAuthKeysPath = null;
|
|
348
|
+
if (process.platform === 'win32') {
|
|
349
|
+
const programData = process.env.PROGRAMDATA || 'C:\\ProgramData';
|
|
350
|
+
const adminKeysPath = path.join(programData, 'ssh', 'administrators_authorized_keys');
|
|
351
|
+
try {
|
|
352
|
+
if (fs.existsSync(path.dirname(adminKeysPath))) {
|
|
353
|
+
await fs.promises.appendFile(adminKeysPath, `\n${authorizedKey}\n`, { mode: 0o600 });
|
|
354
|
+
try {
|
|
355
|
+
await execa('icacls', [adminKeysPath, '/inheritance:r', '/grant', '*S-1-5-32-544:F', '/grant', '*S-1-5-18:F']);
|
|
356
|
+
} catch {}
|
|
357
|
+
adminAuthKeysPath = adminKeysPath;
|
|
358
|
+
}
|
|
359
|
+
} catch {}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return { authKeysPath, adminAuthKeysPath, authorizedKey };
|
|
232
363
|
}
|
|
233
364
|
|
|
234
|
-
async function removePublicKey(authKeysPath, authorizedKey) {
|
|
365
|
+
async function removePublicKey(authKeysPath, authorizedKey, adminAuthKeysPath = null) {
|
|
235
366
|
if (fs.existsSync(authKeysPath)) {
|
|
236
367
|
const stat = await fs.promises.lstat(authKeysPath);
|
|
237
|
-
if (stat.isSymbolicLink()) {
|
|
238
|
-
|
|
368
|
+
if (!stat.isSymbolicLink()) {
|
|
369
|
+
let keys = await fs.promises.readFile(authKeysPath, 'utf8');
|
|
370
|
+
keys = keys.replace(`\n${authorizedKey}\n`, '');
|
|
371
|
+
await fs.promises.writeFile(authKeysPath, keys);
|
|
372
|
+
try {
|
|
373
|
+
await fs.promises.chmod(authKeysPath, 0o600);
|
|
374
|
+
} catch {}
|
|
239
375
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
376
|
+
}
|
|
377
|
+
if (adminAuthKeysPath && fs.existsSync(adminAuthKeysPath)) {
|
|
378
|
+
try {
|
|
379
|
+
let keys = await fs.promises.readFile(adminAuthKeysPath, 'utf8');
|
|
380
|
+
keys = keys.replace(`\n${authorizedKey}\n`, '');
|
|
381
|
+
await fs.promises.writeFile(adminAuthKeysPath, keys);
|
|
382
|
+
} catch {}
|
|
244
383
|
}
|
|
245
384
|
}
|
|
246
385
|
|
|
@@ -347,12 +486,19 @@ async function startLocalHostDashboard(uid, password, serviceConfig, sessionStat
|
|
|
347
486
|
async function fetchDecryptedApprovals() {
|
|
348
487
|
const data = await fetchApprovalRequests(BROKER_URL, uid, sessionState.hostToken);
|
|
349
488
|
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 };
|
|
489
|
+
const base = { id: a.id, status: a.status, createdAt: a.createdAt, decidedAt: a.decidedAt, ip: a.ip || 'unknown' };
|
|
351
490
|
if (!a.iv || !a.ciphertext || !a.salt) return base;
|
|
352
491
|
try {
|
|
353
492
|
const decrypted = await decryptAsync(a.iv, a.ciphertext, password, a.salt);
|
|
354
493
|
const details = JSON.parse(decrypted);
|
|
355
|
-
return {
|
|
494
|
+
return {
|
|
495
|
+
...base,
|
|
496
|
+
username: details.username,
|
|
497
|
+
hostname: details.hostname,
|
|
498
|
+
os: details.os,
|
|
499
|
+
intent: details.intent,
|
|
500
|
+
localIp: details.localIp || 'unknown'
|
|
501
|
+
};
|
|
356
502
|
} catch {
|
|
357
503
|
return base;
|
|
358
504
|
}
|
|
@@ -488,7 +634,31 @@ async function startLocalHostDashboard(uid, password, serviceConfig, sessionStat
|
|
|
488
634
|
const { requestId, decision } = req.body || {};
|
|
489
635
|
if (!requestId || !decision) return res.status(400).json({ error: 'requestId and decision required' });
|
|
490
636
|
try {
|
|
491
|
-
|
|
637
|
+
let approvedPayload = null;
|
|
638
|
+
if (decision === 'approved') {
|
|
639
|
+
const data = await fetchApprovalRequests(BROKER_URL, uid, sessionState.hostToken);
|
|
640
|
+
const request = (data.approvals || []).find(item => item.id === requestId);
|
|
641
|
+
if (!request) return res.status(404).json({ error: 'Request not found' });
|
|
642
|
+
|
|
643
|
+
let details = {};
|
|
644
|
+
try {
|
|
645
|
+
details = JSON.parse(await decryptAsync(request.iv, request.ciphertext, password, request.salt));
|
|
646
|
+
} catch {}
|
|
647
|
+
|
|
648
|
+
const clientKeySalt = [
|
|
649
|
+
password,
|
|
650
|
+
request.ip || 'unknown',
|
|
651
|
+
details.username || 'unknown',
|
|
652
|
+
details.hostname || 'unknown',
|
|
653
|
+
details.os || 'unknown'
|
|
654
|
+
].join('|');
|
|
655
|
+
const clientPwd = crypto.createHash('sha256').update(clientKeySalt).digest('hex');
|
|
656
|
+
|
|
657
|
+
const payload = JSON.stringify({ url: sessionState.tunnelUrl, ...serviceConfig });
|
|
658
|
+
approvedPayload = await encryptAsync(payload, clientPwd);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
await decideApprovalRequest(BROKER_URL, uid, requestId, decision, sessionState.hostToken, approvedPayload);
|
|
492
662
|
recordEvent('approval_decision', { uid, requestId, decision, via: 'dashboard' });
|
|
493
663
|
res.json({ ok: true });
|
|
494
664
|
} catch (err) {
|
|
@@ -684,7 +854,7 @@ function renderApprovals(approvals) {
|
|
|
684
854
|
heading.append(title, badge);
|
|
685
855
|
const details = document.createElement('div');
|
|
686
856
|
details.className = 'meta';
|
|
687
|
-
details.textContent = 'User: ' + String(req.username || 'unknown') + ' | Host: ' + String(req.hostname || 'unknown') + ' | OS: ' + String(req.os || 'unknown');
|
|
857
|
+
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') + ')';
|
|
688
858
|
const meta = document.createElement('div');
|
|
689
859
|
meta.className = 'meta';
|
|
690
860
|
meta.textContent = 'Submitted: ' + new Date(req.createdAt).toLocaleTimeString();
|
|
@@ -932,7 +1102,8 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
932
1102
|
const choices = [
|
|
933
1103
|
{ name: '✅ Review pending client approvals', value: 'approvals' },
|
|
934
1104
|
{ name: '📡 See detailed client telemetry', value: 'show' },
|
|
935
|
-
{ name: '
|
|
1105
|
+
{ name: '📄 View live client activity logs', value: 'logs' },
|
|
1106
|
+
{ name: '📄 View host session activity logs', value: 'host_logs' },
|
|
936
1107
|
{ name: '🔄 Re-register with broker', value: 'reregister' }
|
|
937
1108
|
];
|
|
938
1109
|
|
|
@@ -975,10 +1146,18 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
975
1146
|
|
|
976
1147
|
for (const request of pending) {
|
|
977
1148
|
let details = {};
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1149
|
+
if (!request.iv || !request.ciphertext || !request.salt) {
|
|
1150
|
+
console.log(chalk.yellow(' ⚠️ Broker did not return encrypted client metadata.'));
|
|
1151
|
+
console.log(chalk.dim(' This usually means the broker is running an older version.'));
|
|
1152
|
+
console.log(chalk.dim(' Redeploy the broker with the latest server.js to fix this.'));
|
|
1153
|
+
details = { error: 'Broker returned no encrypted metadata' };
|
|
1154
|
+
} else {
|
|
1155
|
+
try {
|
|
1156
|
+
details = JSON.parse(await decryptAsync(request.iv, request.ciphertext, password, request.salt));
|
|
1157
|
+
} catch (decErr) {
|
|
1158
|
+
console.log(chalk.yellow(` ⚠️ Could not decrypt client details: ${decErr.message}`));
|
|
1159
|
+
details = { error: 'Decryption failed' };
|
|
1160
|
+
}
|
|
982
1161
|
}
|
|
983
1162
|
console.log('');
|
|
984
1163
|
console.log(chalk.bold.cyan(` Approval Request ${request.id}`));
|
|
@@ -986,6 +1165,7 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
986
1165
|
console.log(` Host: ${details.hostname || 'unknown'}`);
|
|
987
1166
|
console.log(` OS: ${details.os || 'unknown'}`);
|
|
988
1167
|
console.log(` Intent: ${details.intent || 'connect'}`);
|
|
1168
|
+
console.log(` IP: ${request.ip || 'unknown'} (Local: ${details.localIp || 'unknown'})`);
|
|
989
1169
|
|
|
990
1170
|
const { decision } = await inquirer.prompt([{
|
|
991
1171
|
type: 'list',
|
|
@@ -998,7 +1178,23 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
998
1178
|
],
|
|
999
1179
|
}]);
|
|
1000
1180
|
if (decision !== 'skip') {
|
|
1001
|
-
|
|
1181
|
+
let approvedPayload = null;
|
|
1182
|
+
if (decision === 'approved') {
|
|
1183
|
+
// Key derivation uses ONLY values both sides reliably know:
|
|
1184
|
+
// password + broker-observed IP. No decrypted client metadata
|
|
1185
|
+
// (which may fail if broker is stale or encryption differs).
|
|
1186
|
+
const clientKeySalt = [
|
|
1187
|
+
password,
|
|
1188
|
+
request.ip || 'unknown',
|
|
1189
|
+
uid
|
|
1190
|
+
].join('|');
|
|
1191
|
+
const clientPwd = crypto.createHash('sha256').update(clientKeySalt).digest('hex');
|
|
1192
|
+
|
|
1193
|
+
const payload = JSON.stringify({ url: sessionState.tunnelUrl, ...serviceConfig });
|
|
1194
|
+
approvedPayload = await encryptAsync(payload, clientPwd);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
await decideApprovalRequest(BROKER_URL, uid, request.id, decision, sessionState.hostToken, approvedPayload);
|
|
1002
1198
|
recordEvent('approval_decision', { uid, requestId: request.id, decision, username: details.username });
|
|
1003
1199
|
}
|
|
1004
1200
|
}
|
|
@@ -1023,6 +1219,14 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
1023
1219
|
}
|
|
1024
1220
|
});
|
|
1025
1221
|
|
|
1222
|
+
addCleanupHook(() => {
|
|
1223
|
+
try {
|
|
1224
|
+
if (chatServerInstance && chatServerInstance.server) {
|
|
1225
|
+
chatServerInstance.server.close();
|
|
1226
|
+
}
|
|
1227
|
+
} catch {}
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1026
1230
|
console.log(chalk.dim(' Provisioning Cloudflare tunnel for chat...'));
|
|
1027
1231
|
chatTunnelProcess = await spawnTunnelSupervised(`http://localhost:${chatServerInstance.port}`, async (newUrl) => {
|
|
1028
1232
|
serviceConfig.chatUrl = newUrl;
|
|
@@ -1041,6 +1245,9 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
1041
1245
|
|
|
1042
1246
|
case 'dashboard': {
|
|
1043
1247
|
dashboardInstance = await startLocalHostDashboard(uid, password, serviceConfig, sessionState);
|
|
1248
|
+
addCleanupHook(() => {
|
|
1249
|
+
try { if (dashboardInstance) dashboardInstance.close(); } catch {}
|
|
1250
|
+
});
|
|
1044
1251
|
logSessionEvent('host_dashboard_opened');
|
|
1045
1252
|
return waitForAction();
|
|
1046
1253
|
}
|
|
@@ -1057,48 +1264,12 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
1057
1264
|
return waitForAction();
|
|
1058
1265
|
}
|
|
1059
1266
|
|
|
1060
|
-
case '
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
console.log('');
|
|
1067
|
-
|
|
1068
|
-
try {
|
|
1069
|
-
await execa('tmux', ['-V'], { reject: true });
|
|
1070
|
-
const sessions = await getMirrorableSessions();
|
|
1071
|
-
if (sessions.length === 0) {
|
|
1072
|
-
console.log(chalk.yellow(' ⚠️ No mirrored terminal session is active yet.'));
|
|
1073
|
-
console.log(chalk.dim(' A client must choose "Connect via SSH" first. SCP-only clients do not create a tmux session.'));
|
|
1074
|
-
console.log(chalk.dim(' tmux is needed on the host machine only; the client does not need tmux.'));
|
|
1075
|
-
logSessionEvent('host_mirror_missing_session', {}, 'warn');
|
|
1076
|
-
return waitForAction();
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
let target = sessions[0];
|
|
1080
|
-
if (sessions.length > 1) {
|
|
1081
|
-
const { sessionChoice } = await inquirer.prompt([{
|
|
1082
|
-
type: 'list',
|
|
1083
|
-
name: 'sessionChoice',
|
|
1084
|
-
message: 'Select an active client session to mirror:',
|
|
1085
|
-
choices: sessions.map((s, idx) => {
|
|
1086
|
-
const created = s.createdAt ? new Date(s.createdAt * 1000).toLocaleTimeString() : 'Unknown';
|
|
1087
|
-
const label = `${s.name} ${chalk.dim(`(started ${created})`)}`;
|
|
1088
|
-
return { name: label, value: String(idx) };
|
|
1089
|
-
}),
|
|
1090
|
-
}]);
|
|
1091
|
-
target = sessions[parseInt(sessionChoice, 10)] || sessions[0];
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
await execa('tmux', [...target.socketArgs, 'attach', '-t', target.name, '-r'], { stdio: 'inherit', reject: false });
|
|
1095
|
-
logSessionEvent('host_mirror_attached', { session: target.name, source: target.source });
|
|
1096
|
-
} catch (err) {
|
|
1097
|
-
console.log(chalk.yellow(' ⚠️ Could not attach to tmux.'));
|
|
1098
|
-
console.log(chalk.dim(` ${err.message}`));
|
|
1099
|
-
console.log(chalk.dim(' Terminal mirroring requires tmux on the host machine and an active interactive SSH client.'));
|
|
1100
|
-
logSessionEvent('host_mirror_error', { error: err.message }, 'warn');
|
|
1101
|
-
}
|
|
1267
|
+
case 'logs': {
|
|
1268
|
+
await viewLiveClientLogs(serviceConfig.sharedDropPath);
|
|
1269
|
+
return waitForAction();
|
|
1270
|
+
}
|
|
1271
|
+
case 'host_logs': {
|
|
1272
|
+
await viewHostLiveLogs();
|
|
1102
1273
|
return waitForAction();
|
|
1103
1274
|
}
|
|
1104
1275
|
|
|
@@ -1154,15 +1325,7 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
1154
1325
|
try {
|
|
1155
1326
|
await revokeUID(BROKER_URL, uid, sessionState.hostToken);
|
|
1156
1327
|
tunnelProcess.kill();
|
|
1157
|
-
|
|
1158
|
-
await execa('tmux', [...tmuxSocketArgs(), 'kill-server'], { reject: false });
|
|
1159
|
-
const legacySessions = await listTmuxSessions();
|
|
1160
|
-
for (const session of legacySessions) {
|
|
1161
|
-
if (isSecureLinkSession(session.name)) {
|
|
1162
|
-
await execa('tmux', ['kill-session', '-t', session.name], { reject: false });
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1328
|
+
// No tmux server to terminate since mirroring was discarded
|
|
1166
1329
|
spinner.succeed('Session revoked and iPingYou-owned connections terminated');
|
|
1167
1330
|
logSessionEvent('host_sessions_terminated');
|
|
1168
1331
|
} catch {
|
|
@@ -1174,6 +1337,9 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
1174
1337
|
|
|
1175
1338
|
case 'exit':
|
|
1176
1339
|
if (dashboardInstance) dashboardInstance.close();
|
|
1340
|
+
if (chatServerInstance && chatServerInstance.server) {
|
|
1341
|
+
try { chatServerInstance.server.close(); } catch {}
|
|
1342
|
+
}
|
|
1177
1343
|
if (chatTunnelProcess) chatTunnelProcess.kill();
|
|
1178
1344
|
if (global.privateBrokerInstance) global.privateBrokerInstance.kill();
|
|
1179
1345
|
if (tunnelProcess) tunnelProcess.kill();
|
|
@@ -1310,7 +1476,6 @@ export async function startHostMode() {
|
|
|
1310
1476
|
|
|
1311
1477
|
if (serviceType === 'ssh' || serviceType === 'share') {
|
|
1312
1478
|
await ensureSSHRunning();
|
|
1313
|
-
await ensureTmuxInstalled();
|
|
1314
1479
|
|
|
1315
1480
|
try {
|
|
1316
1481
|
serviceConfig.sharedDropPath = await prepareSharedDropFolder(uid);
|
|
@@ -1340,13 +1505,13 @@ export async function startHostMode() {
|
|
|
1340
1505
|
console.log(chalk.dim(' 🔑 Generating ephemeral SSH key for passwordless entry...'));
|
|
1341
1506
|
try {
|
|
1342
1507
|
const ephemeralKey = await generateEphemeralKey();
|
|
1343
|
-
const { authKeysPath, authorizedKey } = await injectPublicKey(ephemeralKey.pubKey);
|
|
1508
|
+
const { authKeysPath, adminAuthKeysPath, authorizedKey } = await injectPublicKey(ephemeralKey.pubKey);
|
|
1344
1509
|
|
|
1345
1510
|
serviceConfig.privateKey = ephemeralKey.privKey;
|
|
1346
1511
|
|
|
1347
1512
|
addCleanupHook(async () => {
|
|
1348
1513
|
console.log(chalk.dim(' Removing ephemeral public key...'));
|
|
1349
|
-
await removePublicKey(authKeysPath, authorizedKey);
|
|
1514
|
+
await removePublicKey(authKeysPath, authorizedKey, adminAuthKeysPath);
|
|
1350
1515
|
try { await fs.promises.unlink(ephemeralKey.keyPath); } catch { }
|
|
1351
1516
|
try { await fs.promises.unlink(`${ephemeralKey.keyPath}.pub`); } catch { }
|
|
1352
1517
|
});
|