@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/ai.js
CHANGED
|
@@ -8,16 +8,16 @@ import inquirer from 'inquirer';
|
|
|
8
8
|
import fs from 'node:fs';
|
|
9
9
|
import os from 'node:os';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
-
import { getAlias } from '../lib/config.js';
|
|
12
|
-
import { resolveUID } from '../lib/broker.js';
|
|
13
|
-
import { buildSshArgs, extractHostname, quoteRemoteShell } from '../lib/ssh.js';
|
|
14
|
-
import { addCleanupHook, cleanupAll } from '../lib/cleanup.js';
|
|
11
|
+
import { getAlias } from '../lib/mod/config.js';
|
|
12
|
+
import { resolveUID } from '../lib/client/broker.js';
|
|
13
|
+
import { buildSshArgs, extractHostname, quoteRemoteShell } from '../lib/services/ssh.js';
|
|
14
|
+
import { addCleanupHook, cleanupAll, trackPID, untrackPID } from '../lib/mod/cleanup.js';
|
|
15
15
|
import { startHostMode } from './host.js';
|
|
16
16
|
import { startClientMode } from './client.js';
|
|
17
17
|
import { performSCPNonInteractive } from './client.js';
|
|
18
18
|
import { DEFAULT_AI_MODEL, createGroqChatCompletion, getGroqApiKey, getRateLimitWarnings, listGroqModels, estimateTokensForMessages } from '../lib/ai/groq.js';
|
|
19
19
|
import { assertSafeReadablePath, classifyCommand, redactSensitive, sanitizeUserTask, truncateForModel } from '../lib/ai/safety.js';
|
|
20
|
-
import { recordEvent } from '../lib/session-log.js';
|
|
20
|
+
import { recordEvent } from '../lib/mod/session-log.js';
|
|
21
21
|
|
|
22
22
|
let BROKER_URL = process.env.BROKER_URL || 'https://ipingyou.onrender.com';
|
|
23
23
|
|
|
@@ -223,11 +223,14 @@ async function runLocalCommand(command) {
|
|
|
223
223
|
if (args.length === 0) {
|
|
224
224
|
return { exitCode: 1, stdout: '', stderr: 'Empty or unsafe command after parsing' };
|
|
225
225
|
}
|
|
226
|
-
const
|
|
226
|
+
const child = execa(args[0], args.slice(1), {
|
|
227
227
|
reject: false,
|
|
228
228
|
timeout: 30000,
|
|
229
229
|
maxBuffer: 1024 * 1024,
|
|
230
230
|
});
|
|
231
|
+
trackPID(child.pid);
|
|
232
|
+
const result = await child;
|
|
233
|
+
untrackPID(child.pid);
|
|
231
234
|
|
|
232
235
|
return {
|
|
233
236
|
exitCode: result.exitCode,
|
|
@@ -290,11 +293,14 @@ export function parseLocalCommand(command) {
|
|
|
290
293
|
async function runRemoteCommand(context, command) {
|
|
291
294
|
const sshArgs = buildSshArgs(context.hostname, context.privateKeyPath);
|
|
292
295
|
sshArgs.push(`${context.username}@${context.hostname}`, command);
|
|
293
|
-
const
|
|
296
|
+
const child = execa('ssh', sshArgs, {
|
|
294
297
|
reject: false,
|
|
295
298
|
timeout: 30000,
|
|
296
299
|
maxBuffer: 1024 * 1024,
|
|
297
300
|
});
|
|
301
|
+
trackPID(child.pid);
|
|
302
|
+
const result = await child;
|
|
303
|
+
untrackPID(child.pid);
|
|
298
304
|
|
|
299
305
|
return {
|
|
300
306
|
exitCode: result.exitCode,
|
|
@@ -650,7 +656,7 @@ async function tryAITransfer(task, context) {
|
|
|
650
656
|
if (context && context.scope === 'remote' && context.hostname && context.username) {
|
|
651
657
|
console.log(chalk.dim(` Using active remote session: ${context.username}@${context.hostname}`));
|
|
652
658
|
|
|
653
|
-
const { buildProxyCommandOption, getSshControlOptions, formatScpRemotePath } = await import('../lib/ssh.js');
|
|
659
|
+
const { buildProxyCommandOption, getSshControlOptions, formatScpRemotePath } = await import('../lib/services/ssh.js');
|
|
654
660
|
const scpArgs = [
|
|
655
661
|
'-r',
|
|
656
662
|
...buildProxyCommandOption(context.hostname),
|
|
@@ -670,7 +676,10 @@ async function tryAITransfer(task, context) {
|
|
|
670
676
|
}
|
|
671
677
|
|
|
672
678
|
try {
|
|
673
|
-
const
|
|
679
|
+
const child = execa('scp', scpArgs, { stdio: 'inherit', reject: false });
|
|
680
|
+
trackPID(child.pid);
|
|
681
|
+
const result = await child;
|
|
682
|
+
untrackPID(child.pid);
|
|
674
683
|
if (result.exitCode === 0) {
|
|
675
684
|
console.log(chalk.green(' ✅ Transfer completed via active remote session.'));
|
|
676
685
|
recordEvent('ai_transfer_success', { direction, localPath, remotePath, hostname: context.hostname, reusedContext: true });
|
package/src/modes/client.js
CHANGED
|
@@ -18,20 +18,73 @@ import inquirer from 'inquirer';
|
|
|
18
18
|
import fs from 'node:fs';
|
|
19
19
|
import path from 'node:path';
|
|
20
20
|
import os from 'node:os';
|
|
21
|
-
import { cleanupAll, trackPID, untrackPID, addCleanupHook } from '../lib/cleanup.js';
|
|
22
|
-
import { createSpinner, sshSpinner, networkSpinner, fileTransferSpinner, showConnectionTrace, simulateTransferProgress } from '../lib/animations.js';
|
|
23
|
-
import { getConfig, saveAlias } from '../lib/config.js';
|
|
24
|
-
import { pushTelemetry, requestHostApproval, resolveUID, revokeUID, waitForApproval } from '../lib/broker.js';
|
|
25
|
-
import { calculateChecksum } from '../lib/checksum.js';
|
|
26
|
-
import { promptLocalPath, promptRemotePath } from '../lib/path-browser.js';
|
|
27
|
-
import { buildProxyCommandOption, buildSshArgs, extractHostname, formatScpRemotePath, getKnownHostsOptions, getSshControlOptions, quoteRemoteShell } from '../lib/ssh.js';
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import { cleanupSessionLog, initSessionLog, logSessionEvent, recordEvent } from '../lib/session-log.js';
|
|
21
|
+
import { cleanupAll, trackPID, untrackPID, addCleanupHook } from '../lib/mod/cleanup.js';
|
|
22
|
+
import { createSpinner, sshSpinner, networkSpinner, fileTransferSpinner, showConnectionTrace, simulateTransferProgress } from '../lib/mod/animations.js';
|
|
23
|
+
import { getConfig, saveAlias } from '../lib/mod/config.js';
|
|
24
|
+
import { pushTelemetry, requestHostApproval, resolveUID, revokeUID, waitForApproval } from '../lib/client/broker.js';
|
|
25
|
+
import { calculateChecksum } from '../lib/mod/checksum.js';
|
|
26
|
+
import { promptLocalPath, promptRemotePath } from '../lib/client/path-browser.js';
|
|
27
|
+
import { buildProxyCommandOption, buildSshArgs, extractHostname, formatScpRemotePath, getKnownHostsOptions, getSshControlOptions, quoteRemoteShell } from '../lib/services/ssh.js';
|
|
28
|
+
import { openUrl } from '../lib/mod/open-url.js';
|
|
29
|
+
import { secureSensitiveUrl } from '../lib/mod/secure-print.js';
|
|
30
|
+
import { cleanupSessionLog, initSessionLog, logSessionEvent, recordEvent } from '../lib/mod/session-log.js';
|
|
32
31
|
|
|
33
32
|
let BROKER_URL = process.env.BROKER_URL || 'https://ipingyou.onrender.com';
|
|
34
33
|
|
|
34
|
+
function startLiveLogSync(username, hostname, privateKeyPath, remoteDropPath, localLogPath, persistKnownHosts = true) {
|
|
35
|
+
if (!remoteDropPath || !localLogPath) return;
|
|
36
|
+
|
|
37
|
+
let lastSize = -1;
|
|
38
|
+
let lastMtime = 0;
|
|
39
|
+
let isSyncing = false;
|
|
40
|
+
let consecutiveFailures = 0;
|
|
41
|
+
let warnedOnce = false;
|
|
42
|
+
const interval = setInterval(async () => {
|
|
43
|
+
if (isSyncing) return;
|
|
44
|
+
isSyncing = true;
|
|
45
|
+
try {
|
|
46
|
+
if (!fs.existsSync(localLogPath)) return;
|
|
47
|
+
const stats = fs.statSync(localLogPath);
|
|
48
|
+
if (stats.size === lastSize && stats.mtimeMs === lastMtime) return;
|
|
49
|
+
|
|
50
|
+
const scpArgs = [
|
|
51
|
+
'-O',
|
|
52
|
+
...buildProxyCommandOption(hostname),
|
|
53
|
+
...getKnownHostsOptions(persistKnownHosts),
|
|
54
|
+
'-o', 'IdentitiesOnly=yes',
|
|
55
|
+
...getSshControlOptions(hostname)
|
|
56
|
+
];
|
|
57
|
+
if (privateKeyPath) {
|
|
58
|
+
scpArgs.push('-i', privateKeyPath, '-o', 'IdentityAgent=none');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const clientName = `${os.userInfo().username}-${os.hostname()}`;
|
|
62
|
+
const remoteFilePath = `${remoteDropPath}/client-${clientName}.log`;
|
|
63
|
+
|
|
64
|
+
scpArgs.push(localLogPath, `${username}@${hostname}:${formatScpRemotePath(remoteFilePath)}`);
|
|
65
|
+
|
|
66
|
+
const result = await execa('scp', scpArgs, { reject: false });
|
|
67
|
+
if (result.exitCode === 0) {
|
|
68
|
+
lastSize = stats.size;
|
|
69
|
+
lastMtime = stats.mtimeMs;
|
|
70
|
+
consecutiveFailures = 0;
|
|
71
|
+
} else {
|
|
72
|
+
consecutiveFailures++;
|
|
73
|
+
if (consecutiveFailures >= 5 && !warnedOnce) {
|
|
74
|
+
warnedOnce = true;
|
|
75
|
+
logSessionEvent('client_log_sync_failing', { failures: consecutiveFailures, stderr: (result.stderr || '').slice(0, 200) }, 'warn');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
consecutiveFailures++;
|
|
80
|
+
} finally {
|
|
81
|
+
isSyncing = false;
|
|
82
|
+
}
|
|
83
|
+
}, 3000);
|
|
84
|
+
|
|
85
|
+
addCleanupHook(() => clearInterval(interval));
|
|
86
|
+
}
|
|
87
|
+
|
|
35
88
|
async function promptUsername() {
|
|
36
89
|
const { username } = await inquirer.prompt([
|
|
37
90
|
{
|
|
@@ -94,18 +147,11 @@ async function connectSSH(username, hostname, privateKeyPath, persistKnownHosts
|
|
|
94
147
|
|
|
95
148
|
const sshArgs = buildSshArgs(hostname, privateKeyPath, [
|
|
96
149
|
'-o', 'ServerAliveInterval=30',
|
|
97
|
-
'-o', 'ServerAliveCountMax=3'
|
|
150
|
+
'-o', 'ServerAliveCountMax=3',
|
|
151
|
+
'-t'
|
|
98
152
|
], { persistKnownHosts });
|
|
99
153
|
|
|
100
154
|
sshArgs.push(`${username}@${hostname}`);
|
|
101
|
-
const tmuxSession = buildTmuxSessionName(username);
|
|
102
|
-
const quotedSocket = quoteRemoteShell(TMUX_SOCKET_PATH);
|
|
103
|
-
const quotedSession = quoteRemoteShell(tmuxSession);
|
|
104
|
-
const tmuxSocketCmdStr = `tmux -S ${quotedSocket}`;
|
|
105
|
-
const tmuxPrepare = `${tmuxSocketCmdStr} has-session -t ${quotedSession} 2>/dev/null || ${tmuxSocketCmdStr} new-session -d -s ${quotedSession}`;
|
|
106
|
-
const tmuxAttach = `${tmuxSocketCmdStr} attach -t ${quotedSession}`;
|
|
107
|
-
const tmuxCommand = `if command -v tmux >/dev/null 2>&1; then (${tmuxPrepare} && ${tmuxAttach}) || exec $SHELL -l; else exec $SHELL -l; fi`;
|
|
108
|
-
sshArgs.push('-t', tmuxCommand);
|
|
109
155
|
|
|
110
156
|
const child = execa('ssh', sshArgs, {
|
|
111
157
|
stdio: 'inherit',
|
|
@@ -199,6 +245,7 @@ async function performSCP(username, hostname, direction, privateKeyPath, sharedD
|
|
|
199
245
|
// Construct SCP args
|
|
200
246
|
const scpArgs = [
|
|
201
247
|
'-r', // recursive just in case
|
|
248
|
+
'-O', // Force legacy SCP protocol so that shell quoting in formatScpRemotePath works correctly
|
|
202
249
|
...buildProxyCommandOption(hostname),
|
|
203
250
|
...getKnownHostsOptions(persistKnownHosts),
|
|
204
251
|
'-o', 'IdentitiesOnly=yes',
|
|
@@ -293,6 +340,7 @@ async function downloadSpecificRemotePath(username, hostname, privateKeyPath, re
|
|
|
293
340
|
await showConnectionTrace('Local', 'Remote SCP');
|
|
294
341
|
const scpArgs = [
|
|
295
342
|
'-r',
|
|
343
|
+
'-O', // Force legacy SCP protocol
|
|
296
344
|
...buildProxyCommandOption(hostname),
|
|
297
345
|
...getKnownHostsOptions(persistKnownHosts),
|
|
298
346
|
'-o', 'IdentitiesOnly=yes',
|
|
@@ -300,7 +348,10 @@ async function downloadSpecificRemotePath(username, hostname, privateKeyPath, re
|
|
|
300
348
|
];
|
|
301
349
|
if (privateKeyPath) scpArgs.push('-i', privateKeyPath, '-o', 'IdentityAgent=none');
|
|
302
350
|
scpArgs.push(`${username}@${hostname}:${formatScpRemotePath(remotePath)}`, localPath);
|
|
303
|
-
const
|
|
351
|
+
const child = execa('scp', scpArgs, { stdio: 'inherit', reject: false });
|
|
352
|
+
trackPID(child.pid);
|
|
353
|
+
const result = await child;
|
|
354
|
+
untrackPID(child.pid);
|
|
304
355
|
return result.exitCode === 0;
|
|
305
356
|
}
|
|
306
357
|
|
|
@@ -435,12 +486,27 @@ export async function startClientMode(options = {}) {
|
|
|
435
486
|
console.log(chalk.dim(' The host has enabled approval gating. Submitting your access request...'));
|
|
436
487
|
|
|
437
488
|
try {
|
|
489
|
+
let localIp = '127.0.0.1';
|
|
490
|
+
try {
|
|
491
|
+
const interfaces = os.networkInterfaces();
|
|
492
|
+
for (const devName in interfaces) {
|
|
493
|
+
const iface = interfaces[devName];
|
|
494
|
+
for (const alias of iface) {
|
|
495
|
+
if (alias.family === 'IPv4' && !alias.internal) {
|
|
496
|
+
localIp = alias.address;
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
} catch {}
|
|
502
|
+
|
|
438
503
|
const approvalDetails = {
|
|
439
504
|
username: os.userInfo().username,
|
|
440
505
|
hostname: os.hostname(),
|
|
441
506
|
os: `${os.type()} ${os.release()} (${os.arch()})`,
|
|
442
507
|
intent: 'connect',
|
|
443
508
|
time: new Date().toISOString(),
|
|
509
|
+
localIp,
|
|
444
510
|
};
|
|
445
511
|
|
|
446
512
|
const { requestId, status: reqStatus, approvalRequired } = await requestHostApproval(
|
|
@@ -457,9 +523,9 @@ export async function startClientMode(options = {}) {
|
|
|
457
523
|
console.log(chalk.dim(' This may take a few minutes. Press Ctrl+C to cancel.'));
|
|
458
524
|
console.log('');
|
|
459
525
|
|
|
460
|
-
const
|
|
526
|
+
const approvalResult = await waitForApproval(BROKER_URL, targetUid, requestId, 300000);
|
|
461
527
|
|
|
462
|
-
if (approved) {
|
|
528
|
+
if (approvalResult && approvalResult.approved) {
|
|
463
529
|
console.log(chalk.green(' ✅ Host approved your access request!'));
|
|
464
530
|
logSessionEvent('client_approval_granted', { uid: targetUid, requestId });
|
|
465
531
|
payload = await resolveUID(BROKER_URL, targetUid, targetPassword, false, requestId);
|
|
@@ -560,6 +626,15 @@ export async function startClientMode(options = {}) {
|
|
|
560
626
|
}
|
|
561
627
|
}
|
|
562
628
|
|
|
629
|
+
// Push telemetry immediately so host can see the client in "See detailed client telemetry"
|
|
630
|
+
// even before the user picks an action (SSH/SCP/etc.)
|
|
631
|
+
await pushTelemetry(BROKER_URL, targetUid, targetPassword, username, 'connected');
|
|
632
|
+
|
|
633
|
+
// Start background E2E client log sync if sharedDropPath is configured
|
|
634
|
+
if (payload.sharedDropPath && sessionLogPath) {
|
|
635
|
+
startLiveLogSync(username, hostname, privateKeyPath, payload.sharedDropPath, sessionLogPath, persistKnownHosts);
|
|
636
|
+
}
|
|
637
|
+
|
|
563
638
|
// ─── One-Time File Share Auto-Download ────────────────────
|
|
564
639
|
if (payload.oneTime && payload.oneTimeSharePath) {
|
|
565
640
|
console.log('');
|
|
@@ -684,7 +759,7 @@ export async function performSCPNonInteractive(params = {}) {
|
|
|
684
759
|
const privateKeyPath = payload.privateKey ? await writeEphemeralPrivateKey(payload.privateKey) : null;
|
|
685
760
|
|
|
686
761
|
// Build scp args similar to performSCP
|
|
687
|
-
const scpArgs = ['-r', ...buildProxyCommandOption(hostname), ...getKnownHostsOptions(persistKnownHosts), '-o', 'IdentitiesOnly=yes', ...getSshControlOptions(hostname)];
|
|
762
|
+
const scpArgs = ['-r', '-O', ...buildProxyCommandOption(hostname), ...getKnownHostsOptions(persistKnownHosts), '-o', 'IdentitiesOnly=yes', ...getSshControlOptions(hostname)];
|
|
688
763
|
if (privateKeyPath) scpArgs.push('-i', privateKeyPath, '-o', 'IdentityAgent=none');
|
|
689
764
|
|
|
690
765
|
const remoteSpec = `${username}@${hostname}:${formatScpRemotePath(remotePath)}`;
|
|
@@ -695,7 +770,10 @@ export async function performSCPNonInteractive(params = {}) {
|
|
|
695
770
|
}
|
|
696
771
|
|
|
697
772
|
try {
|
|
698
|
-
const
|
|
773
|
+
const child = execa('scp', scpArgs, { stdio: 'inherit', reject: false });
|
|
774
|
+
trackPID(child.pid);
|
|
775
|
+
const result = await child;
|
|
776
|
+
untrackPID(child.pid);
|
|
699
777
|
if (result.exitCode === 0) {
|
|
700
778
|
recordEvent('scp_transfer_success', { direction, localPath, remotePath, hostname, automated: true });
|
|
701
779
|
return true;
|
package/src/modes/doctor.js
CHANGED
|
@@ -7,8 +7,8 @@ import chalk from 'chalk';
|
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import os from 'node:os';
|
|
9
9
|
import path from 'node:path';
|
|
10
|
-
import { commandExists, detectOS } from '../lib/platform.js';
|
|
11
|
-
import { pingBroker } from '../lib/broker.js';
|
|
10
|
+
import { commandExists, detectOS, isLinuxSSHActive, getCloudflaredPath } from '../lib/services/platform.js';
|
|
11
|
+
import { pingBroker } from '../lib/client/broker.js';
|
|
12
12
|
import { classifyCommand, redactSensitive } from '../lib/ai/safety.js';
|
|
13
13
|
|
|
14
14
|
let BROKER_URL = process.env.BROKER_URL || 'https://ipingyou.onrender.com';
|
|
@@ -96,13 +96,12 @@ async function checkSshService() {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
if (osInfo.isLinux) {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
if (sshd.exitCode === 0) return { status: 'pass', detail: 'SSH service is active' };
|
|
99
|
+
const active = await isLinuxSSHActive();
|
|
100
|
+
if (active) return { status: 'pass', detail: 'SSH service is active' };
|
|
102
101
|
return {
|
|
103
102
|
status: 'warn',
|
|
104
103
|
detail: 'SSH service is not reported active',
|
|
105
|
-
hint: 'Run `sudo systemctl start ssh
|
|
104
|
+
hint: 'Run `sudo systemctl start ssh`, `sudo service ssh start` or equivalent before hosting.',
|
|
106
105
|
};
|
|
107
106
|
}
|
|
108
107
|
|
|
@@ -231,8 +230,8 @@ export async function startDoctorMode(options = {}) {
|
|
|
231
230
|
await check('ssh client', () => commandVersion('ssh', ['-V']));
|
|
232
231
|
await check('scp client', () => commandFound('scp'));
|
|
233
232
|
await check('ssh-keygen', () => commandFound('ssh-keygen'));
|
|
234
|
-
|
|
235
|
-
await check('
|
|
233
|
+
const cfPath = await getCloudflaredPath();
|
|
234
|
+
await check('cloudflared', () => commandVersion(cfPath || 'cloudflared', ['--version']));
|
|
236
235
|
|
|
237
236
|
console.log(chalk.bold('\n Host readiness'));
|
|
238
237
|
await check('SSH service', checkSshService);
|