@miraj181/ipingyou 2.1.19 → 2.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -2
- package/package.json +1 -1
- package/src/cli.js +22 -14
- package/src/lib/ai/safety.js +1 -1
- package/src/lib/{broker.js → client/broker.js} +24 -8
- package/src/lib/{path-browser.js → client/path-browser.js} +6 -2
- package/src/lib/{session-log.js → mod/session-log.js} +17 -8
- package/src/lib/{worker-runtime.js → mod/worker-runtime.js} +1 -1
- package/src/lib/{chat.js → services/chat.js} +2 -2
- package/src/lib/services/platform.js +364 -0
- package/src/lib/{tunnel.js → services/tunnel.js} +2 -2
- package/src/modes/ai.js +18 -9
- package/src/modes/client.js +90 -25
- package/src/modes/doctor.js +5 -7
- package/src/modes/host.js +303 -153
- package/src/server.js +50 -4
- package/src/lib/platform.js +0 -90
- /package/src/lib/{allowlist.js → mod/allowlist.js} +0 -0
- /package/src/lib/{animations.js → mod/animations.js} +0 -0
- /package/src/lib/{checksum.js → mod/checksum.js} +0 -0
- /package/src/lib/{cleanup.js → mod/cleanup.js} +0 -0
- /package/src/lib/{config.js → mod/config.js} +0 -0
- /package/src/lib/{crypto.js → mod/crypto.js} +0 -0
- /package/src/lib/{open-url.js → mod/open-url.js} +0 -0
- /package/src/lib/{secure-print.js → mod/secure-print.js} +0 -0
- /package/src/lib/{socket-firewall.js → mod/socket-firewall.js} +0 -0
- /package/src/lib/{tmux.js → mod/tmux.js} +0 -0
- /package/src/lib/{uid.js → mod/uid.js} +0 -0
- /package/src/lib/{ssh.js → services/ssh.js} +0 -0
package/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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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('');
|
|
245
|
+
|
|
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
|
+
}
|
|
179
258
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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;
|
|
@@ -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
|
|
|
@@ -347,12 +480,19 @@ async function startLocalHostDashboard(uid, password, serviceConfig, sessionStat
|
|
|
347
480
|
async function fetchDecryptedApprovals() {
|
|
348
481
|
const data = await fetchApprovalRequests(BROKER_URL, uid, sessionState.hostToken);
|
|
349
482
|
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 };
|
|
483
|
+
const base = { id: a.id, status: a.status, createdAt: a.createdAt, decidedAt: a.decidedAt, ip: a.ip || 'unknown' };
|
|
351
484
|
if (!a.iv || !a.ciphertext || !a.salt) return base;
|
|
352
485
|
try {
|
|
353
486
|
const decrypted = await decryptAsync(a.iv, a.ciphertext, password, a.salt);
|
|
354
487
|
const details = JSON.parse(decrypted);
|
|
355
|
-
return {
|
|
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
|
+
};
|
|
356
496
|
} catch {
|
|
357
497
|
return base;
|
|
358
498
|
}
|
|
@@ -488,7 +628,31 @@ async function startLocalHostDashboard(uid, password, serviceConfig, sessionStat
|
|
|
488
628
|
const { requestId, decision } = req.body || {};
|
|
489
629
|
if (!requestId || !decision) return res.status(400).json({ error: 'requestId and decision required' });
|
|
490
630
|
try {
|
|
491
|
-
|
|
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);
|
|
492
656
|
recordEvent('approval_decision', { uid, requestId, decision, via: 'dashboard' });
|
|
493
657
|
res.json({ ok: true });
|
|
494
658
|
} catch (err) {
|
|
@@ -684,7 +848,7 @@ function renderApprovals(approvals) {
|
|
|
684
848
|
heading.append(title, badge);
|
|
685
849
|
const details = document.createElement('div');
|
|
686
850
|
details.className = 'meta';
|
|
687
|
-
details.textContent = 'User: ' + String(req.username || 'unknown') + ' | Host: ' + String(req.hostname || 'unknown') + ' | OS: ' + String(req.os || 'unknown');
|
|
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') + ')';
|
|
688
852
|
const meta = document.createElement('div');
|
|
689
853
|
meta.className = 'meta';
|
|
690
854
|
meta.textContent = 'Submitted: ' + new Date(req.createdAt).toLocaleTimeString();
|
|
@@ -932,7 +1096,8 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
932
1096
|
const choices = [
|
|
933
1097
|
{ name: '✅ Review pending client approvals', value: 'approvals' },
|
|
934
1098
|
{ name: '📡 See detailed client telemetry', value: 'show' },
|
|
935
|
-
{ name: '
|
|
1099
|
+
{ name: '📄 View live client activity logs', value: 'logs' },
|
|
1100
|
+
{ name: '📄 View host session activity logs', value: 'host_logs' },
|
|
936
1101
|
{ name: '🔄 Re-register with broker', value: 'reregister' }
|
|
937
1102
|
];
|
|
938
1103
|
|
|
@@ -986,6 +1151,7 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
986
1151
|
console.log(` Host: ${details.hostname || 'unknown'}`);
|
|
987
1152
|
console.log(` OS: ${details.os || 'unknown'}`);
|
|
988
1153
|
console.log(` Intent: ${details.intent || 'connect'}`);
|
|
1154
|
+
console.log(` IP: ${request.ip || 'unknown'} (Local: ${details.localIp || 'unknown'})`);
|
|
989
1155
|
|
|
990
1156
|
const { decision } = await inquirer.prompt([{
|
|
991
1157
|
type: 'list',
|
|
@@ -998,7 +1164,22 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
998
1164
|
],
|
|
999
1165
|
}]);
|
|
1000
1166
|
if (decision !== 'skip') {
|
|
1001
|
-
|
|
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);
|
|
1002
1183
|
recordEvent('approval_decision', { uid, requestId: request.id, decision, username: details.username });
|
|
1003
1184
|
}
|
|
1004
1185
|
}
|
|
@@ -1023,6 +1204,14 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
1023
1204
|
}
|
|
1024
1205
|
});
|
|
1025
1206
|
|
|
1207
|
+
addCleanupHook(() => {
|
|
1208
|
+
try {
|
|
1209
|
+
if (chatServerInstance && chatServerInstance.server) {
|
|
1210
|
+
chatServerInstance.server.close();
|
|
1211
|
+
}
|
|
1212
|
+
} catch {}
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1026
1215
|
console.log(chalk.dim(' Provisioning Cloudflare tunnel for chat...'));
|
|
1027
1216
|
chatTunnelProcess = await spawnTunnelSupervised(`http://localhost:${chatServerInstance.port}`, async (newUrl) => {
|
|
1028
1217
|
serviceConfig.chatUrl = newUrl;
|
|
@@ -1041,6 +1230,9 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
1041
1230
|
|
|
1042
1231
|
case 'dashboard': {
|
|
1043
1232
|
dashboardInstance = await startLocalHostDashboard(uid, password, serviceConfig, sessionState);
|
|
1233
|
+
addCleanupHook(() => {
|
|
1234
|
+
try { if (dashboardInstance) dashboardInstance.close(); } catch {}
|
|
1235
|
+
});
|
|
1044
1236
|
logSessionEvent('host_dashboard_opened');
|
|
1045
1237
|
return waitForAction();
|
|
1046
1238
|
}
|
|
@@ -1057,48 +1249,12 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
1057
1249
|
return waitForAction();
|
|
1058
1250
|
}
|
|
1059
1251
|
|
|
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
|
-
}
|
|
1252
|
+
case 'logs': {
|
|
1253
|
+
await viewLiveClientLogs(serviceConfig.sharedDropPath);
|
|
1254
|
+
return waitForAction();
|
|
1255
|
+
}
|
|
1256
|
+
case 'host_logs': {
|
|
1257
|
+
await viewHostLiveLogs();
|
|
1102
1258
|
return waitForAction();
|
|
1103
1259
|
}
|
|
1104
1260
|
|
|
@@ -1154,15 +1310,7 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
1154
1310
|
try {
|
|
1155
1311
|
await revokeUID(BROKER_URL, uid, sessionState.hostToken);
|
|
1156
1312
|
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
|
-
}
|
|
1313
|
+
// No tmux server to terminate since mirroring was discarded
|
|
1166
1314
|
spinner.succeed('Session revoked and iPingYou-owned connections terminated');
|
|
1167
1315
|
logSessionEvent('host_sessions_terminated');
|
|
1168
1316
|
} catch {
|
|
@@ -1174,6 +1322,9 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
1174
1322
|
|
|
1175
1323
|
case 'exit':
|
|
1176
1324
|
if (dashboardInstance) dashboardInstance.close();
|
|
1325
|
+
if (chatServerInstance && chatServerInstance.server) {
|
|
1326
|
+
try { chatServerInstance.server.close(); } catch {}
|
|
1327
|
+
}
|
|
1177
1328
|
if (chatTunnelProcess) chatTunnelProcess.kill();
|
|
1178
1329
|
if (global.privateBrokerInstance) global.privateBrokerInstance.kill();
|
|
1179
1330
|
if (tunnelProcess) tunnelProcess.kill();
|
|
@@ -1310,7 +1461,6 @@ export async function startHostMode() {
|
|
|
1310
1461
|
|
|
1311
1462
|
if (serviceType === 'ssh' || serviceType === 'share') {
|
|
1312
1463
|
await ensureSSHRunning();
|
|
1313
|
-
await ensureTmuxInstalled();
|
|
1314
1464
|
|
|
1315
1465
|
try {
|
|
1316
1466
|
serviceConfig.sharedDropPath = await prepareSharedDropFolder(uid);
|
|
@@ -1340,13 +1490,13 @@ export async function startHostMode() {
|
|
|
1340
1490
|
console.log(chalk.dim(' 🔑 Generating ephemeral SSH key for passwordless entry...'));
|
|
1341
1491
|
try {
|
|
1342
1492
|
const ephemeralKey = await generateEphemeralKey();
|
|
1343
|
-
const { authKeysPath, authorizedKey } = await injectPublicKey(ephemeralKey.pubKey);
|
|
1493
|
+
const { authKeysPath, adminAuthKeysPath, authorizedKey } = await injectPublicKey(ephemeralKey.pubKey);
|
|
1344
1494
|
|
|
1345
1495
|
serviceConfig.privateKey = ephemeralKey.privKey;
|
|
1346
1496
|
|
|
1347
1497
|
addCleanupHook(async () => {
|
|
1348
1498
|
console.log(chalk.dim(' Removing ephemeral public key...'));
|
|
1349
|
-
await removePublicKey(authKeysPath, authorizedKey);
|
|
1499
|
+
await removePublicKey(authKeysPath, authorizedKey, adminAuthKeysPath);
|
|
1350
1500
|
try { await fs.promises.unlink(ephemeralKey.keyPath); } catch { }
|
|
1351
1501
|
try { await fs.promises.unlink(`${ephemeralKey.keyPath}.pub`); } catch { }
|
|
1352
1502
|
});
|