@miraj181/ipingyou 2.0.14 → 2.1.1
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 +8 -1
- package/package.json +7 -6
- package/src/cli.js +216 -0
- package/src/lib/ai/groq.js +116 -0
- package/src/lib/ai/safety.js +114 -0
- package/src/lib/allowlist.js +35 -0
- package/src/lib/broker.js +139 -13
- package/src/lib/cleanup.js +14 -6
- package/src/lib/config.js +8 -3
- package/src/lib/path-browser.js +2 -2
- package/src/lib/platform.js +0 -31
- package/src/lib/session-log.js +93 -0
- package/src/modes/ai.js +627 -0
- package/src/modes/client.js +288 -31
- package/src/modes/doctor.js +293 -0
- package/src/modes/host.js +485 -38
- package/src/server.js +226 -14
package/src/modes/client.js
CHANGED
|
@@ -21,11 +21,12 @@ import os from 'node:os';
|
|
|
21
21
|
import { cleanupAll, trackPID, untrackPID, addCleanupHook } from '../lib/cleanup.js';
|
|
22
22
|
import { createSpinner, sshSpinner, networkSpinner, fileTransferSpinner, showConnectionTrace, simulateTransferProgress } from '../lib/animations.js';
|
|
23
23
|
import { getConfig, saveAlias } from '../lib/config.js';
|
|
24
|
-
import { pushTelemetry, resolveUID } from '../lib/broker.js';
|
|
24
|
+
import { pushTelemetry, requestHostApproval, resolveUID, revokeUID, waitForApproval } from '../lib/broker.js';
|
|
25
25
|
import { calculateChecksum } from '../lib/checksum.js';
|
|
26
26
|
import { promptLocalPath, promptRemotePath } from '../lib/path-browser.js';
|
|
27
27
|
import { buildSshArgs, extractHostname, formatScpRemotePath, getSshControlOptions, quoteRemoteShell } from '../lib/ssh.js';
|
|
28
28
|
import open from 'open';
|
|
29
|
+
import { cleanupSessionLog, initSessionLog, logSessionEvent, recordEvent } from '../lib/session-log.js';
|
|
29
30
|
|
|
30
31
|
let BROKER_URL = process.env.BROKER_URL || 'https://ipingyou.onrender.com';
|
|
31
32
|
|
|
@@ -57,7 +58,7 @@ async function writeEphemeralPrivateKey(privateKey) {
|
|
|
57
58
|
});
|
|
58
59
|
|
|
59
60
|
if (result.exitCode !== 0) {
|
|
60
|
-
try { fs.unlinkSync(keyPath); } catch {}
|
|
61
|
+
try { fs.unlinkSync(keyPath); } catch { }
|
|
61
62
|
throw new Error(result.stderr.trim() || 'OpenSSH could not parse the host-provided private key');
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -100,12 +101,15 @@ async function connectSSH(username, hostname, privateKeyPath) {
|
|
|
100
101
|
if (result.exitCode === 0) {
|
|
101
102
|
console.log('');
|
|
102
103
|
console.log(chalk.green(' ✅ SSH session ended cleanly'));
|
|
104
|
+
recordEvent('ssh_session_ended', { hostname, exitCode: 0 });
|
|
103
105
|
} else if (result.exitCode === 255) {
|
|
104
106
|
console.log('');
|
|
105
107
|
console.error(chalk.red(' ❌ SSH connection failed (exit code 255)'));
|
|
108
|
+
recordEvent('ssh_session_failed', { hostname, exitCode: 255 });
|
|
106
109
|
} else {
|
|
107
110
|
console.log('');
|
|
108
111
|
console.error(chalk.red(` ❌ SSH exited with code ${result.exitCode}`));
|
|
112
|
+
recordEvent('ssh_session_ended', { hostname, exitCode: result.exitCode });
|
|
109
113
|
}
|
|
110
114
|
} catch (err) {
|
|
111
115
|
console.error(chalk.red(` ❌ SSH error: ${err.message}`));
|
|
@@ -115,7 +119,48 @@ async function connectSSH(username, hostname, privateKeyPath) {
|
|
|
115
119
|
/**
|
|
116
120
|
* Perform an SCP file transfer through the Cloudflare tunnel.
|
|
117
121
|
*/
|
|
118
|
-
async function
|
|
122
|
+
async function chooseRemoteTransferPath(username, hostname, privateKeyPath, direction, sharedDropPath) {
|
|
123
|
+
if (!sharedDropPath) {
|
|
124
|
+
return promptRemotePath(username, hostname, privateKeyPath, direction === 'upload' ? 'destination' : 'source');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const { dropChoice } = await inquirer.prompt([{
|
|
128
|
+
type: 'list',
|
|
129
|
+
name: 'dropChoice',
|
|
130
|
+
message: direction === 'upload'
|
|
131
|
+
? 'Where should the file/folder go on the host?'
|
|
132
|
+
: 'Where should browsing start on the host?',
|
|
133
|
+
choices: direction === 'upload'
|
|
134
|
+
? [
|
|
135
|
+
{ name: `📥 Use host shared drop folder (${sharedDropPath})`, value: 'drop' },
|
|
136
|
+
{ name: '🔍 Browse host folders', value: 'browse' },
|
|
137
|
+
{ name: '⌨️ Type host destination path manually', value: 'manual' }
|
|
138
|
+
]
|
|
139
|
+
: [
|
|
140
|
+
{ name: `📥 Start in host shared drop folder (${sharedDropPath})`, value: 'drop_browse' },
|
|
141
|
+
{ name: '🔍 Browse from host home folder', value: 'browse' },
|
|
142
|
+
{ name: '⌨️ Type host file/folder path manually', value: 'manual' }
|
|
143
|
+
]
|
|
144
|
+
}]);
|
|
145
|
+
|
|
146
|
+
if (dropChoice === 'drop') return sharedDropPath;
|
|
147
|
+
if (dropChoice === 'drop_browse') {
|
|
148
|
+
return promptRemotePath(username, hostname, privateKeyPath, 'source', sharedDropPath);
|
|
149
|
+
}
|
|
150
|
+
if (dropChoice === 'manual') {
|
|
151
|
+
const { remotePath } = await inquirer.prompt([{
|
|
152
|
+
type: 'input',
|
|
153
|
+
name: 'remotePath',
|
|
154
|
+
message: direction === 'upload' ? 'Host destination path:' : 'Host file/folder path:',
|
|
155
|
+
validate: v => v.trim().length > 0 || 'Required',
|
|
156
|
+
}]);
|
|
157
|
+
return remotePath.trim();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return promptRemotePath(username, hostname, privateKeyPath, direction === 'upload' ? 'destination' : 'source');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function performSCP(username, hostname, direction, privateKeyPath, sharedDropPath = null) {
|
|
119
164
|
console.log('');
|
|
120
165
|
console.log(chalk.bold(` 📦 SCP Transfer (${direction})`));
|
|
121
166
|
console.log(chalk.dim(' ─────────────────────────────────'));
|
|
@@ -124,11 +169,11 @@ async function performSCP(username, hostname, direction, privateKeyPath) {
|
|
|
124
169
|
let remotePath;
|
|
125
170
|
|
|
126
171
|
if (direction === 'upload') {
|
|
127
|
-
remotePath = await
|
|
172
|
+
remotePath = await chooseRemoteTransferPath(username, hostname, privateKeyPath, direction, sharedDropPath);
|
|
128
173
|
localPath = await promptLocalPath('client file/folder to upload');
|
|
129
174
|
} else {
|
|
130
175
|
localPath = await promptLocalPath('client destination');
|
|
131
|
-
remotePath = await
|
|
176
|
+
remotePath = await chooseRemoteTransferPath(username, hostname, privateKeyPath, direction, sharedDropPath);
|
|
132
177
|
}
|
|
133
178
|
|
|
134
179
|
await showConnectionTrace('Local', 'Remote SCP');
|
|
@@ -179,7 +224,7 @@ async function performSCP(username, hostname, direction, privateKeyPath) {
|
|
|
179
224
|
|
|
180
225
|
if (result.exitCode === 0) {
|
|
181
226
|
await simulateTransferProgress(direction === 'upload' ? localPath : remotePath, direction, 1500);
|
|
182
|
-
|
|
227
|
+
|
|
183
228
|
// Verify Checksum
|
|
184
229
|
if (direction === 'upload' && localHash) {
|
|
185
230
|
console.log(chalk.dim(' 🔍 Verifying remote SHA-256 checksum...'));
|
|
@@ -193,10 +238,10 @@ async function performSCP(username, hostname, direction, privateKeyPath) {
|
|
|
193
238
|
];
|
|
194
239
|
if (privateKeyPath) sshArgs.push('-i', privateKeyPath, '-o', 'IdentityAgent=none');
|
|
195
240
|
sshArgs.push(`${username}@${hostname}`, `shasum -a 256 ${quoteRemoteShell(remoteChecksumPath)} 2>/dev/null || sha256sum ${quoteRemoteShell(remoteChecksumPath)} 2>/dev/null || shasum -a 256 ${quoteRemoteShell(remotePath)} 2>/dev/null || sha256sum ${quoteRemoteShell(remotePath)}`);
|
|
196
|
-
|
|
241
|
+
|
|
197
242
|
const { stdout } = await execa('ssh', sshArgs, { reject: false });
|
|
198
243
|
const remoteHash = stdout.split(' ')[0].trim();
|
|
199
|
-
|
|
244
|
+
|
|
200
245
|
if (remoteHash === localHash) {
|
|
201
246
|
console.log(chalk.green(` ✅ Zero-Trust File Integrity: Hash match (${remoteHash.substring(0, 16)}...)`));
|
|
202
247
|
} else {
|
|
@@ -212,15 +257,33 @@ async function performSCP(username, hostname, direction, privateKeyPath) {
|
|
|
212
257
|
}
|
|
213
258
|
|
|
214
259
|
console.log(chalk.green(` ✅ Transfer completed successfully!`));
|
|
260
|
+
recordEvent('scp_transfer_success', { direction, localPath, remotePath, hostname });
|
|
215
261
|
} else {
|
|
216
262
|
console.error(chalk.red(' ❌ SCP transfer failed'));
|
|
217
263
|
if (result.stderr) console.error(chalk.dim(` ${result.stderr.trim()}`));
|
|
264
|
+
recordEvent('scp_transfer_failed', { direction, localPath, remotePath, hostname, error: result.stderr });
|
|
218
265
|
}
|
|
219
266
|
} catch (err) {
|
|
220
267
|
console.error(chalk.red(` ❌ SCP error: ${err.message}`));
|
|
221
268
|
}
|
|
222
269
|
}
|
|
223
270
|
|
|
271
|
+
async function downloadSpecificRemotePath(username, hostname, privateKeyPath, remotePath, localPath) {
|
|
272
|
+
await showConnectionTrace('Local', 'Remote SCP');
|
|
273
|
+
const proxyCommand = `cloudflared access tcp --hostname ${hostname}`;
|
|
274
|
+
const scpArgs = [
|
|
275
|
+
'-r',
|
|
276
|
+
'-o', `ProxyCommand=${proxyCommand}`,
|
|
277
|
+
'-o', 'StrictHostKeyChecking=accept-new',
|
|
278
|
+
'-o', 'IdentitiesOnly=yes',
|
|
279
|
+
...getSshControlOptions(hostname),
|
|
280
|
+
];
|
|
281
|
+
if (privateKeyPath) scpArgs.push('-i', privateKeyPath, '-o', 'IdentityAgent=none');
|
|
282
|
+
scpArgs.push(`${username}@${hostname}:${formatScpRemotePath(remotePath)}`, localPath);
|
|
283
|
+
const result = await execa('scp', scpArgs, { stdio: 'inherit', reject: false });
|
|
284
|
+
return result.exitCode === 0;
|
|
285
|
+
}
|
|
286
|
+
|
|
224
287
|
function joinRemotePath(parent, child) {
|
|
225
288
|
const cleanParent = String(parent || '').replace(/\/+$/, '');
|
|
226
289
|
if (!cleanParent) return child;
|
|
@@ -237,6 +300,13 @@ export async function startClientMode(options = {}) {
|
|
|
237
300
|
console.log(chalk.dim(' ──────────────────────────────────────────'));
|
|
238
301
|
console.log('');
|
|
239
302
|
|
|
303
|
+
const sessionLogPath = initSessionLog('client');
|
|
304
|
+
if (sessionLogPath) {
|
|
305
|
+
console.log(chalk.dim(` 📜 Session log: ${sessionLogPath}`));
|
|
306
|
+
addCleanupHook(() => cleanupSessionLog());
|
|
307
|
+
}
|
|
308
|
+
logSessionEvent('client_mode_started');
|
|
309
|
+
|
|
240
310
|
// Allow setting a custom broker URL if process.env isn't overridden by CLI
|
|
241
311
|
if (process.env.BROKER_URL) {
|
|
242
312
|
BROKER_URL = process.env.BROKER_URL;
|
|
@@ -264,11 +334,12 @@ export async function startClientMode(options = {}) {
|
|
|
264
334
|
BROKER_URL = customBroker.trim();
|
|
265
335
|
process.env.BROKER_URL = BROKER_URL; // Update for consistency
|
|
266
336
|
}
|
|
337
|
+
logSessionEvent('client_broker_selected', { choice: brokerChoice, broker: BROKER_URL });
|
|
267
338
|
}
|
|
268
339
|
|
|
269
340
|
const config = getConfig();
|
|
270
341
|
const aliasKeys = Object.keys(config.aliases || {});
|
|
271
|
-
|
|
342
|
+
|
|
272
343
|
let targetUid = null;
|
|
273
344
|
let targetPassword = null;
|
|
274
345
|
let targetUsername = null;
|
|
@@ -291,6 +362,7 @@ export async function startClientMode(options = {}) {
|
|
|
291
362
|
targetUid = aliasData.uid;
|
|
292
363
|
targetPassword = aliasData.password;
|
|
293
364
|
targetUsername = aliasData.username;
|
|
365
|
+
logSessionEvent('client_alias_selected', { alias: useAlias, uid: targetUid });
|
|
294
366
|
}
|
|
295
367
|
}
|
|
296
368
|
|
|
@@ -303,6 +375,7 @@ export async function startClientMode(options = {}) {
|
|
|
303
375
|
validate: (v) => v.trim().length > 0 || 'Password is required to decrypt',
|
|
304
376
|
}]);
|
|
305
377
|
targetPassword = password.trim();
|
|
378
|
+
logSessionEvent('client_uid_provided', { uid: targetUid });
|
|
306
379
|
}
|
|
307
380
|
|
|
308
381
|
if (!targetUid) {
|
|
@@ -327,10 +400,61 @@ export async function startClientMode(options = {}) {
|
|
|
327
400
|
]);
|
|
328
401
|
targetUid = answer.uid.trim();
|
|
329
402
|
targetPassword = answer.password.trim();
|
|
403
|
+
logSessionEvent('client_uid_provided', { uid: targetUid });
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
let payload = await resolveUID(BROKER_URL, targetUid, targetPassword);
|
|
407
|
+
|
|
408
|
+
// ─── Approval Flow ──────────────────────────────────────────
|
|
409
|
+
if (payload && payload.needsApproval) {
|
|
410
|
+
console.log('');
|
|
411
|
+
console.log(chalk.bold.yellow(' 🔐 Host Approval Required'));
|
|
412
|
+
console.log(chalk.dim(' ──────────────────────────────────────'));
|
|
413
|
+
console.log(chalk.dim(' The host has enabled approval gating. Submitting your access request...'));
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
const approvalDetails = {
|
|
417
|
+
username: os.userInfo().username,
|
|
418
|
+
hostname: os.hostname(),
|
|
419
|
+
os: `${os.type()} ${os.release()} (${os.arch()})`,
|
|
420
|
+
intent: 'connect',
|
|
421
|
+
time: new Date().toISOString(),
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const { requestId, status: reqStatus, approvalRequired } = await requestHostApproval(
|
|
425
|
+
BROKER_URL, targetUid, targetPassword, approvalDetails
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
logSessionEvent('client_approval_submitted', { uid: targetUid, requestId });
|
|
429
|
+
|
|
430
|
+
if (!approvalRequired || reqStatus === 'approved') {
|
|
431
|
+
console.log(chalk.green(' ✅ Access auto-approved (host does not require manual approval for this session).'));
|
|
432
|
+
payload = await resolveUID(BROKER_URL, targetUid, targetPassword, false, requestId);
|
|
433
|
+
} else {
|
|
434
|
+
console.log(chalk.yellow(` ⏳ Waiting for host to approve your request (ID: ${requestId})...`));
|
|
435
|
+
console.log(chalk.dim(' This may take a few minutes. Press Ctrl+C to cancel.'));
|
|
436
|
+
console.log('');
|
|
437
|
+
|
|
438
|
+
const approved = await waitForApproval(BROKER_URL, targetUid, requestId, 300000);
|
|
439
|
+
|
|
440
|
+
if (approved) {
|
|
441
|
+
console.log(chalk.green(' ✅ Host approved your access request!'));
|
|
442
|
+
logSessionEvent('client_approval_granted', { uid: targetUid, requestId });
|
|
443
|
+
payload = await resolveUID(BROKER_URL, targetUid, targetPassword, false, requestId);
|
|
444
|
+
} else {
|
|
445
|
+
console.log(chalk.red(' ❌ Host denied your access request.'));
|
|
446
|
+
logSessionEvent('client_approval_denied', { uid: targetUid, requestId });
|
|
447
|
+
process.exit(1);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
} catch (err) {
|
|
451
|
+
console.log(chalk.red(` ❌ Approval flow failed: ${err.message}`));
|
|
452
|
+
logSessionEvent('client_approval_error', { uid: targetUid, error: err.message }, 'error');
|
|
453
|
+
process.exit(1);
|
|
454
|
+
}
|
|
330
455
|
}
|
|
331
456
|
|
|
332
|
-
|
|
333
|
-
if (!payload) {
|
|
457
|
+
if (!payload || payload.needsApproval) {
|
|
334
458
|
process.exit(1);
|
|
335
459
|
}
|
|
336
460
|
|
|
@@ -338,9 +462,17 @@ export async function startClientMode(options = {}) {
|
|
|
338
462
|
console.log('');
|
|
339
463
|
console.log(chalk.bold(' 🌐 HTTP Service Exposed'));
|
|
340
464
|
console.log(chalk.dim(' ─────────────────────────────────'));
|
|
341
|
-
console.log(chalk.green(`
|
|
465
|
+
console.log(chalk.green(` Opening in browser: ${chalk.bold.cyan(payload.url)}`));
|
|
342
466
|
console.log('');
|
|
343
|
-
|
|
467
|
+
logSessionEvent('client_http_mode', { uid: targetUid, port: payload.port || null });
|
|
468
|
+
try {
|
|
469
|
+
await open(payload.url);
|
|
470
|
+
} catch {
|
|
471
|
+
console.log(chalk.dim(` Could not auto-open. Please visit: ${payload.url}`));
|
|
472
|
+
}
|
|
473
|
+
console.log(chalk.dim(' Press Ctrl+C to exit.'));
|
|
474
|
+
// Keep process alive so cleanup handlers work
|
|
475
|
+
await new Promise(() => {});
|
|
344
476
|
}
|
|
345
477
|
|
|
346
478
|
if (payload.type === 'tcp') {
|
|
@@ -348,12 +480,40 @@ export async function startClientMode(options = {}) {
|
|
|
348
480
|
console.log(chalk.bold(' 🔌 Custom TCP Port Exposed'));
|
|
349
481
|
console.log(chalk.dim(' ─────────────────────────────────'));
|
|
350
482
|
console.log(` The host is exposing a generic TCP service on port ${payload.port}.`);
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
483
|
+
|
|
484
|
+
const hostname = extractHostname(payload.url);
|
|
485
|
+
const localPort = payload.port;
|
|
486
|
+
|
|
487
|
+
console.log(chalk.dim(` Starting local cloudflared proxy to ${hostname}...`));
|
|
355
488
|
console.log('');
|
|
356
|
-
|
|
489
|
+
|
|
490
|
+
try {
|
|
491
|
+
const { execa: execaFn } = await import('execa');
|
|
492
|
+
const child = execaFn('cloudflared', ['access', 'tcp', '--hostname', hostname, '--url', `127.0.0.1:${localPort}`], {
|
|
493
|
+
stdio: 'inherit',
|
|
494
|
+
reject: false,
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
trackPID(child.pid);
|
|
498
|
+
console.log(chalk.green(` ✅ Local proxy active! Connect your client to ${chalk.bold('127.0.0.1:' + localPort)}`));
|
|
499
|
+
console.log(chalk.dim(' Press Ctrl+C to terminate the tunnel.'));
|
|
500
|
+
logSessionEvent('client_tcp_mode', { uid: targetUid, port: localPort, hostname });
|
|
501
|
+
|
|
502
|
+
const result = await child;
|
|
503
|
+
untrackPID(child.pid);
|
|
504
|
+
|
|
505
|
+
if (result.exitCode !== 0) {
|
|
506
|
+
console.log(chalk.red(` ❌ Cloudflared exited with code ${result.exitCode}`));
|
|
507
|
+
}
|
|
508
|
+
} catch (err) {
|
|
509
|
+
console.log(chalk.red(` ❌ Could not start cloudflared proxy: ${err.message}`));
|
|
510
|
+
console.log(chalk.dim(' Fallback: run manually in another terminal:'));
|
|
511
|
+
console.log(chalk.cyan(` cloudflared access tcp --hostname ${hostname} --url 127.0.0.1:${localPort}`));
|
|
512
|
+
console.log('');
|
|
513
|
+
console.log(` Then connect your local client to ${chalk.green('127.0.0.1:' + localPort)}`);
|
|
514
|
+
}
|
|
515
|
+
await cleanupAll();
|
|
516
|
+
return;
|
|
357
517
|
}
|
|
358
518
|
|
|
359
519
|
const tunnelUrl = payload.url;
|
|
@@ -367,15 +527,56 @@ export async function startClientMode(options = {}) {
|
|
|
367
527
|
try {
|
|
368
528
|
privateKeyPath = await writeEphemeralPrivateKey(payload.privateKey);
|
|
369
529
|
addCleanupHook(() => {
|
|
370
|
-
try { fs.unlinkSync(privateKeyPath); } catch {}
|
|
530
|
+
try { fs.unlinkSync(privateKeyPath); } catch { }
|
|
371
531
|
});
|
|
532
|
+
logSessionEvent('client_ephemeral_key_ready');
|
|
372
533
|
} catch (err) {
|
|
373
534
|
console.log(chalk.yellow(` ⚠️ Could not use ephemeral SSH key: ${err.message}`));
|
|
374
535
|
console.log(chalk.dim(' Falling back to standard OS password.'));
|
|
536
|
+
logSessionEvent('client_ephemeral_key_failed', { error: err.message }, 'warn');
|
|
375
537
|
privateKeyPath = null;
|
|
376
538
|
}
|
|
377
539
|
}
|
|
378
540
|
|
|
541
|
+
// ─── One-Time File Share Auto-Download ────────────────────
|
|
542
|
+
if (payload.oneTime && payload.oneTimeSharePath) {
|
|
543
|
+
console.log('');
|
|
544
|
+
console.log(chalk.bold.magenta(' 📦 One-Time File Share'));
|
|
545
|
+
console.log(chalk.dim(' ──────────────────────────────────────'));
|
|
546
|
+
console.log(chalk.dim(` Host is sharing: ${chalk.cyan(payload.oneTimeSharePath)}`));
|
|
547
|
+
console.log(chalk.dim(' This is a one-time transfer — the session will be revoked after download.'));
|
|
548
|
+
console.log('');
|
|
549
|
+
|
|
550
|
+
const localDest = await promptLocalPath('download destination');
|
|
551
|
+
|
|
552
|
+
await pushTelemetry(BROKER_URL, targetUid, targetPassword, username, 'one-time-download');
|
|
553
|
+
logSessionEvent('client_one_time_download_start', { uid: targetUid, remotePath: payload.oneTimeSharePath });
|
|
554
|
+
|
|
555
|
+
const success = await downloadSpecificRemotePath(username, hostname, privateKeyPath, payload.oneTimeSharePath, localDest);
|
|
556
|
+
|
|
557
|
+
if (success) {
|
|
558
|
+
console.log(chalk.green(' ✅ One-time file transfer completed successfully!'));
|
|
559
|
+
|
|
560
|
+
// Verify download checksum
|
|
561
|
+
const dlHash = await calculateChecksum(localDest);
|
|
562
|
+
if (dlHash) console.log(chalk.green(` 🔍 File Intact. SHA-256: ${dlHash.substring(0, 16)}...`));
|
|
563
|
+
|
|
564
|
+
// Auto-revoke the UID from broker
|
|
565
|
+
console.log(chalk.dim(' Revoking session from broker...'));
|
|
566
|
+
await revokeUID(BROKER_URL, targetUid);
|
|
567
|
+
console.log(chalk.green(' 🔒 Session revoked. No further access is possible.'));
|
|
568
|
+
|
|
569
|
+
logSessionEvent('client_one_time_download_complete', { uid: targetUid });
|
|
570
|
+
recordEvent('one_time_transfer_complete', { uid: targetUid, localDest });
|
|
571
|
+
} else {
|
|
572
|
+
console.log(chalk.red(' ❌ One-time file transfer failed.'));
|
|
573
|
+
logSessionEvent('client_one_time_download_failed', { uid: targetUid }, 'error');
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
await cleanupAll();
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
379
580
|
// Ask to save alias if we entered manually
|
|
380
581
|
if (!targetUsername) {
|
|
381
582
|
const { saveIt } = await inquirer.prompt([{
|
|
@@ -394,12 +595,10 @@ export async function startClientMode(options = {}) {
|
|
|
394
595
|
}]);
|
|
395
596
|
saveAlias(aliasName.trim(), { uid: targetUid, password: targetPassword, username });
|
|
396
597
|
console.log(chalk.green(` ✓ Saved as alias: ${chalk.bold(aliasName.trim())}\n`));
|
|
598
|
+
logSessionEvent('client_alias_saved', { alias: aliasName.trim(), uid: targetUid });
|
|
397
599
|
}
|
|
398
600
|
}
|
|
399
601
|
|
|
400
|
-
// Push secure telemetry to host
|
|
401
|
-
await pushTelemetry(BROKER_URL, targetUid, targetPassword, username);
|
|
402
|
-
|
|
403
602
|
const { action } = await inquirer.prompt([
|
|
404
603
|
{
|
|
405
604
|
type: 'list',
|
|
@@ -415,6 +614,9 @@ export async function startClientMode(options = {}) {
|
|
|
415
614
|
}
|
|
416
615
|
]);
|
|
417
616
|
|
|
617
|
+
await pushTelemetry(BROKER_URL, targetUid, targetPassword, username, action);
|
|
618
|
+
logSessionEvent('client_action_selected', { action });
|
|
619
|
+
|
|
418
620
|
if (action === 'chat') {
|
|
419
621
|
await handleClientChat(targetUid, targetPassword, payload.chatUrl);
|
|
420
622
|
} else if (action === 'ssh') {
|
|
@@ -422,7 +624,7 @@ export async function startClientMode(options = {}) {
|
|
|
422
624
|
} else if (action === 'reverse') {
|
|
423
625
|
await performReverseForward(username, hostname, privateKeyPath);
|
|
424
626
|
} else {
|
|
425
|
-
await performSCP(username, hostname, action, privateKeyPath);
|
|
627
|
+
await performSCP(username, hostname, action, privateKeyPath, payload.sharedDropPath);
|
|
426
628
|
}
|
|
427
629
|
|
|
428
630
|
console.log('');
|
|
@@ -436,16 +638,57 @@ export async function startClientMode(options = {}) {
|
|
|
436
638
|
]);
|
|
437
639
|
|
|
438
640
|
if (reconnect) {
|
|
439
|
-
await handleSubsequentActions(username, hostname, privateKeyPath, targetUid, targetPassword);
|
|
641
|
+
await handleSubsequentActions(username, hostname, privateKeyPath, targetUid, targetPassword, payload.sharedDropPath);
|
|
440
642
|
}
|
|
441
643
|
|
|
644
|
+
logSessionEvent('client_session_exit');
|
|
442
645
|
await cleanupAll();
|
|
443
646
|
}
|
|
444
647
|
|
|
648
|
+
/**
|
|
649
|
+
* Perform a non-interactive SCP transfer (used by AI Transfer Assistant).
|
|
650
|
+
* params: { brokerUrl, uid, password, username, direction, localPath, remotePath }
|
|
651
|
+
*/
|
|
652
|
+
export async function performSCPNonInteractive(params = {}) {
|
|
653
|
+
const { brokerUrl, uid, password, username, direction, localPath, remotePath } = params;
|
|
654
|
+
if (!brokerUrl || !uid || !password) throw new Error('Missing broker connection info');
|
|
655
|
+
|
|
656
|
+
const payload = await resolveUID(brokerUrl, uid, password);
|
|
657
|
+
if (!payload) throw new Error('Could not resolve UID/payload');
|
|
658
|
+
|
|
659
|
+
const tunnelUrl = payload.url;
|
|
660
|
+
const hostname = extractHostname(tunnelUrl);
|
|
661
|
+
const privateKeyPath = payload.privateKey ? await writeEphemeralPrivateKey(payload.privateKey) : null;
|
|
662
|
+
|
|
663
|
+
// Build scp args similar to performSCP
|
|
664
|
+
const proxyCommand = `cloudflared access tcp --hostname ${hostname}`;
|
|
665
|
+
const scpArgs = ['-r', '-o', `ProxyCommand=${proxyCommand}`, '-o', 'StrictHostKeyChecking=accept-new', '-o', 'IdentitiesOnly=yes', ...getSshControlOptions(hostname)];
|
|
666
|
+
if (privateKeyPath) scpArgs.push('-i', privateKeyPath, '-o', 'IdentityAgent=none');
|
|
667
|
+
|
|
668
|
+
const remoteSpec = `${username}@${hostname}:${formatScpRemotePath(remotePath)}`;
|
|
669
|
+
if (direction === 'upload') {
|
|
670
|
+
scpArgs.push(localPath, remoteSpec);
|
|
671
|
+
} else {
|
|
672
|
+
scpArgs.push(remoteSpec, localPath);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
try {
|
|
676
|
+
const result = await execa('scp', scpArgs, { stdio: 'inherit', reject: false });
|
|
677
|
+
if (result.exitCode === 0) {
|
|
678
|
+
recordEvent('scp_transfer_success', { direction, localPath, remotePath, hostname, automated: true });
|
|
679
|
+
return true;
|
|
680
|
+
}
|
|
681
|
+
recordEvent('scp_transfer_failed', { direction, localPath, remotePath, hostname, error: result.stderr, automated: true });
|
|
682
|
+
return false;
|
|
683
|
+
} finally {
|
|
684
|
+
try { if (privateKeyPath) fs.unlinkSync(privateKeyPath); } catch { }
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
445
688
|
async function handleClientChat(uid, password, cachedChatUrl) {
|
|
446
689
|
let chatUrl = cachedChatUrl;
|
|
447
690
|
const spinner = createSpinner('Checking for active chat room...', networkSpinner).start();
|
|
448
|
-
|
|
691
|
+
|
|
449
692
|
const payload = await resolveUID(BROKER_URL, uid, password, true); // true = silent if possible, or just re-resolve
|
|
450
693
|
if (payload && payload.chatUrl) {
|
|
451
694
|
chatUrl = payload.chatUrl;
|
|
@@ -464,7 +707,7 @@ async function handleClientChat(uid, password, cachedChatUrl) {
|
|
|
464
707
|
}
|
|
465
708
|
}
|
|
466
709
|
|
|
467
|
-
async function handleSubsequentActions(username, hostname, privateKeyPath, targetUid, targetPassword) {
|
|
710
|
+
async function handleSubsequentActions(username, hostname, privateKeyPath, targetUid, targetPassword, sharedDropPath = null) {
|
|
468
711
|
const { action } = await inquirer.prompt([
|
|
469
712
|
{
|
|
470
713
|
type: 'list',
|
|
@@ -483,6 +726,9 @@ async function handleSubsequentActions(username, hostname, privateKeyPath, targe
|
|
|
483
726
|
|
|
484
727
|
if (action === 'exit') return;
|
|
485
728
|
|
|
729
|
+
await pushTelemetry(BROKER_URL, targetUid, targetPassword, username, action);
|
|
730
|
+
logSessionEvent('client_action_selected', { action });
|
|
731
|
+
|
|
486
732
|
if (action === 'chat') {
|
|
487
733
|
await handleClientChat(targetUid, targetPassword, null);
|
|
488
734
|
} else if (action === 'ssh') {
|
|
@@ -490,7 +736,7 @@ async function handleSubsequentActions(username, hostname, privateKeyPath, targe
|
|
|
490
736
|
} else if (action === 'reverse') {
|
|
491
737
|
await performReverseForward(username, hostname, privateKeyPath);
|
|
492
738
|
} else {
|
|
493
|
-
await performSCP(username, hostname, action, privateKeyPath);
|
|
739
|
+
await performSCP(username, hostname, action, privateKeyPath, sharedDropPath);
|
|
494
740
|
}
|
|
495
741
|
|
|
496
742
|
const { reconnect } = await inquirer.prompt([
|
|
@@ -503,7 +749,7 @@ async function handleSubsequentActions(username, hostname, privateKeyPath, targe
|
|
|
503
749
|
]);
|
|
504
750
|
|
|
505
751
|
if (reconnect) {
|
|
506
|
-
await handleSubsequentActions(username, hostname, privateKeyPath, targetUid, targetPassword);
|
|
752
|
+
await handleSubsequentActions(username, hostname, privateKeyPath, targetUid, targetPassword, sharedDropPath);
|
|
507
753
|
}
|
|
508
754
|
}
|
|
509
755
|
|
|
@@ -542,16 +788,27 @@ async function performReverseForward(username, hostname, privateKeyPath) {
|
|
|
542
788
|
console.log('');
|
|
543
789
|
const spinner = createSpinner(`Forwarding Host:${remotePort} ➔ Localhost:${localPort}...`, networkSpinner).start();
|
|
544
790
|
|
|
791
|
+
let child = null;
|
|
545
792
|
try {
|
|
546
|
-
|
|
793
|
+
child = execa('ssh', sshArgs, { stdio: 'inherit', reject: false });
|
|
547
794
|
trackPID(child.pid);
|
|
548
795
|
spinner.succeed(`Reverse tunnel active! Host can access your app at ${chalk.bold.green('localhost:' + remotePort)}`);
|
|
549
796
|
console.log(chalk.dim(' Press Ctrl+C to terminate the reverse tunnel.'));
|
|
550
|
-
|
|
551
|
-
|
|
797
|
+
|
|
798
|
+
recordEvent('reverse_forward_started', { localPort, remotePort, hostname });
|
|
799
|
+
const result = await child;
|
|
800
|
+
if (result.exitCode === 0) {
|
|
801
|
+
recordEvent('reverse_forward_ended', { localPort, remotePort, hostname, exitCode: 0 });
|
|
802
|
+
} else {
|
|
803
|
+
console.log(chalk.red(`\n ❌ Reverse tunnel exited with code ${result.exitCode}`));
|
|
804
|
+
recordEvent('reverse_forward_failed', { localPort, remotePort, hostname, exitCode: result.exitCode });
|
|
805
|
+
}
|
|
552
806
|
} catch (err) {
|
|
553
807
|
if (err.isCanceled) return;
|
|
554
808
|
if (err.killed) return;
|
|
809
|
+
spinner.fail('Reverse tunnel failed to start');
|
|
555
810
|
console.log(chalk.red(`\n ❌ Tunnel disconnected: ${err.message}`));
|
|
811
|
+
} finally {
|
|
812
|
+
if (child?.pid) untrackPID(child.pid);
|
|
556
813
|
}
|
|
557
814
|
}
|