@miraj181/ipingyou 2.1.6 → 2.1.15
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/package.json +8 -7
- package/src/cli.js +31 -28
- package/src/lib/broker.js +18 -9
- package/src/lib/chat.js +9 -2
- package/src/lib/checksum.js +22 -2
- package/src/lib/cleanup.js +18 -8
- package/src/lib/crypto.js +27 -0
- package/src/lib/platform.js +33 -10
- package/src/lib/secure-print.js +86 -0
- package/src/lib/session-log.js +78 -3
- package/src/lib/ssh.js +25 -5
- package/src/lib/tunnel.js +1 -0
- package/src/lib/worker-runtime.js +81 -0
- package/src/lib/workers/crypto-checksum-worker.js +70 -0
- package/src/modes/ai.js +11 -8
- package/src/modes/client.js +27 -15
- package/src/modes/doctor.js +11 -7
- package/src/modes/host.js +254 -99
- package/src/server.js +95 -18
package/src/lib/ssh.js
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
|
|
3
|
+
const SAFE_HOSTNAME_PATTERN = /^(?=.{1,253}$)(?!.*\.\.)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i;
|
|
4
|
+
|
|
5
|
+
export function assertSafeHostname(hostname, label = 'hostname') {
|
|
6
|
+
const normalized = String(hostname || '').trim().replace(/\.$/, '').toLowerCase();
|
|
7
|
+
if (!normalized || !SAFE_HOSTNAME_PATTERN.test(normalized)) {
|
|
8
|
+
throw new Error(`Invalid ${label}`);
|
|
9
|
+
}
|
|
10
|
+
return normalized;
|
|
11
|
+
}
|
|
12
|
+
|
|
3
13
|
export function extractHostname(url) {
|
|
14
|
+
let parsed;
|
|
4
15
|
try {
|
|
5
|
-
|
|
16
|
+
parsed = new URL(url);
|
|
6
17
|
} catch {
|
|
7
|
-
|
|
18
|
+
throw new Error('Invalid tunnel URL');
|
|
8
19
|
}
|
|
20
|
+
if (parsed.protocol !== 'https:') {
|
|
21
|
+
throw new Error('Tunnel URL must use HTTPS');
|
|
22
|
+
}
|
|
23
|
+
return assertSafeHostname(parsed.hostname, 'tunnel hostname');
|
|
9
24
|
}
|
|
10
25
|
|
|
11
26
|
export function quoteRemoteShell(value) {
|
|
@@ -25,8 +40,9 @@ export function formatScpRemotePath(remotePath) {
|
|
|
25
40
|
}
|
|
26
41
|
|
|
27
42
|
export function getSshControlOptions(hostname) {
|
|
43
|
+
const safeHostname = assertSafeHostname(hostname, 'ssh hostname');
|
|
28
44
|
if (process.platform === 'win32') return [];
|
|
29
|
-
const hash = crypto.createHash('
|
|
45
|
+
const hash = crypto.createHash('sha256').update(safeHostname).digest('hex').slice(0, 10);
|
|
30
46
|
return [
|
|
31
47
|
'-o', 'ControlMaster=auto',
|
|
32
48
|
'-o', 'ControlPersist=5m',
|
|
@@ -34,6 +50,11 @@ export function getSshControlOptions(hostname) {
|
|
|
34
50
|
];
|
|
35
51
|
}
|
|
36
52
|
|
|
53
|
+
export function buildProxyCommandOption(hostname) {
|
|
54
|
+
const safeHostname = assertSafeHostname(hostname, 'tunnel hostname');
|
|
55
|
+
return ['-o', `ProxyCommand=cloudflared access tcp --hostname ${safeHostname}`];
|
|
56
|
+
}
|
|
57
|
+
|
|
37
58
|
export function getKnownHostsOptions(persistKnownHosts = true) {
|
|
38
59
|
if (persistKnownHosts) {
|
|
39
60
|
return ['-o', 'StrictHostKeyChecking=accept-new'];
|
|
@@ -48,9 +69,8 @@ export function getKnownHostsOptions(persistKnownHosts = true) {
|
|
|
48
69
|
|
|
49
70
|
export function buildSshArgs(hostname, privateKeyPath, extraOptions = [], options = {}) {
|
|
50
71
|
const { persistKnownHosts = true } = options;
|
|
51
|
-
const proxyCommand = `cloudflared access tcp --hostname ${hostname}`;
|
|
52
72
|
const sshArgs = [
|
|
53
|
-
|
|
73
|
+
...buildProxyCommandOption(hostname),
|
|
54
74
|
...getKnownHostsOptions(persistKnownHosts),
|
|
55
75
|
'-o', 'IdentitiesOnly=yes',
|
|
56
76
|
...getSshControlOptions(hostname),
|
package/src/lib/tunnel.js
CHANGED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Worker } from 'node:worker_threads';
|
|
2
|
+
|
|
3
|
+
const workerUrl = new URL('./workers/crypto-checksum-worker.js', import.meta.url);
|
|
4
|
+
const workersDisabled = process.env.IPINGYOU_DISABLE_WORKERS === '1';
|
|
5
|
+
const MAX_PENDING_TASKS = 128;
|
|
6
|
+
|
|
7
|
+
let worker = null;
|
|
8
|
+
let requestCounter = 0;
|
|
9
|
+
const pending = new Map();
|
|
10
|
+
|
|
11
|
+
function resetWorker(err) {
|
|
12
|
+
for (const { reject } of pending.values()) {
|
|
13
|
+
reject(err);
|
|
14
|
+
}
|
|
15
|
+
pending.clear();
|
|
16
|
+
worker = null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function ensureWorker() {
|
|
20
|
+
if (worker || workersDisabled) return worker;
|
|
21
|
+
|
|
22
|
+
worker = new Worker(workerUrl, {
|
|
23
|
+
resourceLimits: {
|
|
24
|
+
maxOldGenerationSizeMb: 128,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
worker.unref();
|
|
28
|
+
|
|
29
|
+
worker.on('message', (message) => {
|
|
30
|
+
const { id, ok, result, error } = message || {};
|
|
31
|
+
const entry = pending.get(id);
|
|
32
|
+
if (!entry) return;
|
|
33
|
+
pending.delete(id);
|
|
34
|
+
if (pending.size === 0) worker?.unref();
|
|
35
|
+
if (ok) {
|
|
36
|
+
entry.resolve(result);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
entry.reject(new Error(error || 'Worker task failed'));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
worker.on('error', (err) => {
|
|
43
|
+
resetWorker(err);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
worker.on('exit', (code) => {
|
|
47
|
+
if (code !== 0) {
|
|
48
|
+
resetWorker(new Error(`Worker exited with code ${code}`));
|
|
49
|
+
} else {
|
|
50
|
+
worker = null;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return worker;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function canUseWorkers() {
|
|
58
|
+
return !workersDisabled;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function runWorkerTask(type, payload) {
|
|
62
|
+
if (!canUseWorkers()) {
|
|
63
|
+
throw new Error('Worker threads are disabled');
|
|
64
|
+
}
|
|
65
|
+
const activeWorker = ensureWorker();
|
|
66
|
+
if (!activeWorker) {
|
|
67
|
+
throw new Error('Worker is not available');
|
|
68
|
+
}
|
|
69
|
+
if (pending.size >= MAX_PENDING_TASKS) {
|
|
70
|
+
const error = new Error('Worker task queue is at capacity');
|
|
71
|
+
error.code = 'WORKER_QUEUE_FULL';
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const id = ++requestCounter;
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
pending.set(id, { resolve, reject });
|
|
78
|
+
activeWorker.ref();
|
|
79
|
+
activeWorker.postMessage({ id, type, payload });
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { parentPort } from 'node:worker_threads';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
|
|
5
|
+
function deriveKey(password, salt) {
|
|
6
|
+
return crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function encryptPayload(plaintext, password) {
|
|
10
|
+
const salt = crypto.randomBytes(16);
|
|
11
|
+
const key = deriveKey(password, salt);
|
|
12
|
+
const iv = crypto.randomBytes(16);
|
|
13
|
+
|
|
14
|
+
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
|
|
15
|
+
let enc = cipher.update(plaintext, 'utf8', 'base64');
|
|
16
|
+
enc += cipher.final('base64');
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
iv: iv.toString('hex'),
|
|
20
|
+
ciphertext: enc,
|
|
21
|
+
salt: salt.toString('hex'),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function decryptPayload(ivHex, cipherBase64, password, saltHex) {
|
|
26
|
+
const salt = Buffer.from(saltHex, 'hex');
|
|
27
|
+
const key = deriveKey(password, salt);
|
|
28
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
29
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
|
|
30
|
+
let dec = decipher.update(cipherBase64, 'base64', 'utf8');
|
|
31
|
+
dec += decipher.final('utf8');
|
|
32
|
+
return dec;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function checksumFile(filePath) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const hash = crypto.createHash('sha256');
|
|
38
|
+
const stream = fs.createReadStream(filePath);
|
|
39
|
+
stream.on('error', reject);
|
|
40
|
+
stream.on('data', (chunk) => hash.update(chunk));
|
|
41
|
+
stream.on('end', () => resolve(hash.digest('hex')));
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function executeTask(type, payload) {
|
|
46
|
+
switch (type) {
|
|
47
|
+
case 'encrypt':
|
|
48
|
+
return encryptPayload(payload.plaintext, payload.password);
|
|
49
|
+
case 'decrypt':
|
|
50
|
+
return {
|
|
51
|
+
plaintext: decryptPayload(payload.ivHex, payload.cipherBase64, payload.password, payload.saltHex),
|
|
52
|
+
};
|
|
53
|
+
case 'checksum':
|
|
54
|
+
return {
|
|
55
|
+
digest: await checksumFile(payload.filePath),
|
|
56
|
+
};
|
|
57
|
+
default:
|
|
58
|
+
throw new Error(`Unsupported worker task: ${type}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
parentPort.on('message', async (message) => {
|
|
63
|
+
const { id, type, payload } = message || {};
|
|
64
|
+
try {
|
|
65
|
+
const result = await executeTask(type, payload || {});
|
|
66
|
+
parentPort.postMessage({ id, ok: true, result });
|
|
67
|
+
} catch (err) {
|
|
68
|
+
parentPort.postMessage({ id, ok: false, error: err.message });
|
|
69
|
+
}
|
|
70
|
+
});
|
package/src/modes/ai.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* AI Mode — privacy-first local/remote task assistant powered by Groq.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { execa
|
|
5
|
+
import { execa } from 'execa';
|
|
6
|
+
import { parse as shellParse } from 'shell-quote';
|
|
6
7
|
import chalk from 'chalk';
|
|
7
8
|
import inquirer from 'inquirer';
|
|
8
9
|
import fs from 'node:fs';
|
|
@@ -210,8 +211,13 @@ function showRateLimitWarnings(rateLimit) {
|
|
|
210
211
|
}
|
|
211
212
|
|
|
212
213
|
async function runLocalCommand(command) {
|
|
213
|
-
const
|
|
214
|
-
|
|
214
|
+
const parsed = shellParse(command);
|
|
215
|
+
// Filter out non-string tokens (shell operators like |, &&, ; etc.) to prevent injection
|
|
216
|
+
const args = parsed.filter(token => typeof token === 'string');
|
|
217
|
+
if (args.length === 0) {
|
|
218
|
+
return { exitCode: 1, stdout: '', stderr: 'Empty or unsafe command after parsing' };
|
|
219
|
+
}
|
|
220
|
+
const result = await execa(args[0], args.slice(1), {
|
|
215
221
|
reject: false,
|
|
216
222
|
timeout: 30000,
|
|
217
223
|
maxBuffer: 1024 * 1024,
|
|
@@ -568,12 +574,10 @@ async function tryAITransfer(task, context) {
|
|
|
568
574
|
if (context && context.scope === 'remote' && context.hostname && context.username) {
|
|
569
575
|
console.log(chalk.dim(` Using active remote session: ${context.username}@${context.hostname}`));
|
|
570
576
|
|
|
571
|
-
const { getSshControlOptions, formatScpRemotePath } = await import('../lib/ssh.js');
|
|
572
|
-
|
|
573
|
-
const proxyCommand = `cloudflared access tcp --hostname ${context.hostname}`;
|
|
577
|
+
const { buildProxyCommandOption, getSshControlOptions, formatScpRemotePath } = await import('../lib/ssh.js');
|
|
574
578
|
const scpArgs = [
|
|
575
579
|
'-r',
|
|
576
|
-
|
|
580
|
+
...buildProxyCommandOption(context.hostname),
|
|
577
581
|
'-o', 'StrictHostKeyChecking=accept-new',
|
|
578
582
|
'-o', 'IdentitiesOnly=yes',
|
|
579
583
|
...getSshControlOptions(context.hostname),
|
|
@@ -624,4 +628,3 @@ async function tryAITransfer(task, context) {
|
|
|
624
628
|
return true;
|
|
625
629
|
}
|
|
626
630
|
}
|
|
627
|
-
|
package/src/modes/client.js
CHANGED
|
@@ -24,9 +24,10 @@ import { getConfig, saveAlias } from '../lib/config.js';
|
|
|
24
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
|
-
import { buildSshArgs, extractHostname, formatScpRemotePath, getKnownHostsOptions, getSshControlOptions, quoteRemoteShell } from '../lib/ssh.js';
|
|
28
|
-
import { buildTmuxSessionName,
|
|
27
|
+
import { buildProxyCommandOption, buildSshArgs, extractHostname, formatScpRemotePath, getKnownHostsOptions, getSshControlOptions, quoteRemoteShell } from '../lib/ssh.js';
|
|
28
|
+
import { buildTmuxSessionName, TMUX_SOCKET_PATH } from '../lib/tmux.js';
|
|
29
29
|
import open from 'open';
|
|
30
|
+
import { secureSensitiveUrl } from '../lib/secure-print.js';
|
|
30
31
|
import { cleanupSessionLog, initSessionLog, logSessionEvent, recordEvent } from '../lib/session-log.js';
|
|
31
32
|
|
|
32
33
|
let BROKER_URL = process.env.BROKER_URL || 'https://ipingyou.onrender.com';
|
|
@@ -89,9 +90,13 @@ async function connectSSH(username, hostname, privateKeyPath, persistKnownHosts
|
|
|
89
90
|
|
|
90
91
|
sshArgs.push(`${username}@${hostname}`);
|
|
91
92
|
const tmuxSession = buildTmuxSessionName(username);
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
93
|
+
const quotedSocket = quoteRemoteShell(TMUX_SOCKET_PATH);
|
|
94
|
+
const quotedSession = quoteRemoteShell(tmuxSession);
|
|
95
|
+
const tmuxSocketCmdStr = `tmux -S ${quotedSocket}`;
|
|
96
|
+
const tmuxPrepare = `${tmuxSocketCmdStr} has-session -t ${quotedSession} 2>/dev/null || ${tmuxSocketCmdStr} new-session -d -s ${quotedSession}`;
|
|
97
|
+
const tmuxAttach = `${tmuxSocketCmdStr} attach -t ${quotedSession}`;
|
|
98
|
+
const tmuxCommand = `if command -v tmux >/dev/null 2>&1; then (${tmuxPrepare} && ${tmuxAttach}) || exec $SHELL -l; else exec $SHELL -l; fi`;
|
|
99
|
+
sshArgs.push('-t', tmuxCommand);
|
|
95
100
|
|
|
96
101
|
const child = execa('ssh', sshArgs, {
|
|
97
102
|
stdio: 'inherit',
|
|
@@ -182,12 +187,10 @@ async function performSCP(username, hostname, direction, privateKeyPath, sharedD
|
|
|
182
187
|
|
|
183
188
|
await showConnectionTrace('Local', 'Remote SCP');
|
|
184
189
|
|
|
185
|
-
const proxyCommand = `cloudflared access tcp --hostname ${hostname}`;
|
|
186
|
-
|
|
187
190
|
// Construct SCP args
|
|
188
191
|
const scpArgs = [
|
|
189
192
|
'-r', // recursive just in case
|
|
190
|
-
|
|
193
|
+
...buildProxyCommandOption(hostname),
|
|
191
194
|
...getKnownHostsOptions(persistKnownHosts),
|
|
192
195
|
'-o', 'IdentitiesOnly=yes',
|
|
193
196
|
...getSshControlOptions(hostname)
|
|
@@ -218,6 +221,16 @@ async function performSCP(username, hostname, direction, privateKeyPath, sharedD
|
|
|
218
221
|
const child = execa('scp', scpArgs, {
|
|
219
222
|
stdio: ['inherit', 'pipe', 'pipe'],
|
|
220
223
|
reject: false,
|
|
224
|
+
buffer: false,
|
|
225
|
+
});
|
|
226
|
+
const maxDiagnosticBytes = 64 * 1024;
|
|
227
|
+
let stderrOutput = Buffer.alloc(0);
|
|
228
|
+
child.stdout?.resume();
|
|
229
|
+
child.stderr?.on('data', (chunk) => {
|
|
230
|
+
const next = Buffer.concat([stderrOutput, Buffer.from(chunk)]);
|
|
231
|
+
stderrOutput = next.length > maxDiagnosticBytes
|
|
232
|
+
? next.subarray(next.length - maxDiagnosticBytes)
|
|
233
|
+
: next;
|
|
221
234
|
});
|
|
222
235
|
|
|
223
236
|
trackPID(child.pid);
|
|
@@ -257,9 +270,10 @@ async function performSCP(username, hostname, direction, privateKeyPath, sharedD
|
|
|
257
270
|
console.log(chalk.green(` ✅ Transfer completed successfully!`));
|
|
258
271
|
recordEvent('scp_transfer_success', { direction, localPath, remotePath, hostname });
|
|
259
272
|
} else {
|
|
273
|
+
const stderr = stderrOutput.toString('utf8');
|
|
260
274
|
console.error(chalk.red(' ❌ SCP transfer failed'));
|
|
261
|
-
if (
|
|
262
|
-
recordEvent('scp_transfer_failed', { direction, localPath, remotePath, hostname, error:
|
|
275
|
+
if (stderr) console.error(chalk.dim(` ${stderr.trim()}`));
|
|
276
|
+
recordEvent('scp_transfer_failed', { direction, localPath, remotePath, hostname, error: stderr });
|
|
263
277
|
}
|
|
264
278
|
} catch (err) {
|
|
265
279
|
console.error(chalk.red(` ❌ SCP error: ${err.message}`));
|
|
@@ -268,10 +282,9 @@ async function performSCP(username, hostname, direction, privateKeyPath, sharedD
|
|
|
268
282
|
|
|
269
283
|
async function downloadSpecificRemotePath(username, hostname, privateKeyPath, remotePath, localPath, persistKnownHosts = true) {
|
|
270
284
|
await showConnectionTrace('Local', 'Remote SCP');
|
|
271
|
-
const proxyCommand = `cloudflared access tcp --hostname ${hostname}`;
|
|
272
285
|
const scpArgs = [
|
|
273
286
|
'-r',
|
|
274
|
-
|
|
287
|
+
...buildProxyCommandOption(hostname),
|
|
275
288
|
...getKnownHostsOptions(persistKnownHosts),
|
|
276
289
|
'-o', 'IdentitiesOnly=yes',
|
|
277
290
|
...getSshControlOptions(hostname),
|
|
@@ -662,8 +675,7 @@ export async function performSCPNonInteractive(params = {}) {
|
|
|
662
675
|
const privateKeyPath = payload.privateKey ? await writeEphemeralPrivateKey(payload.privateKey) : null;
|
|
663
676
|
|
|
664
677
|
// Build scp args similar to performSCP
|
|
665
|
-
const
|
|
666
|
-
const scpArgs = ['-r', '-o', `ProxyCommand=${proxyCommand}`, ...getKnownHostsOptions(persistKnownHosts), '-o', 'IdentitiesOnly=yes', ...getSshControlOptions(hostname)];
|
|
678
|
+
const scpArgs = ['-r', ...buildProxyCommandOption(hostname), ...getKnownHostsOptions(persistKnownHosts), '-o', 'IdentitiesOnly=yes', ...getSshControlOptions(hostname)];
|
|
667
679
|
if (privateKeyPath) scpArgs.push('-i', privateKeyPath, '-o', 'IdentityAgent=none');
|
|
668
680
|
|
|
669
681
|
const remoteSpec = `${username}@${hostname}:${formatScpRemotePath(remotePath)}`;
|
|
@@ -701,7 +713,7 @@ async function handleClientChat(uid, password, cachedChatUrl) {
|
|
|
701
713
|
const fullUrl = `${chatUrl}#${password}`;
|
|
702
714
|
await open(fullUrl);
|
|
703
715
|
} catch {
|
|
704
|
-
console.log(chalk.cyan(` 👉 Please open: ${chatUrl
|
|
716
|
+
console.log(chalk.cyan(` 👉 Please open: ${secureSensitiveUrl(chatUrl, password)}`));
|
|
705
717
|
}
|
|
706
718
|
} else {
|
|
707
719
|
spinner.warn('The host has not started a chat room yet.');
|
package/src/modes/doctor.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Doctor Mode — non-invasive diagnostics for iPingYou.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { execa
|
|
5
|
+
import { execa } from 'execa';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
7
|
import fs from 'node:fs';
|
|
8
8
|
import os from 'node:os';
|
|
@@ -66,7 +66,7 @@ async function commandFound(command) {
|
|
|
66
66
|
async function checkSshService() {
|
|
67
67
|
const osInfo = detectOS();
|
|
68
68
|
if (osInfo.isMac) {
|
|
69
|
-
const launchctl = await
|
|
69
|
+
const launchctl = await execa('launchctl', ['print-disabled', 'system'], { reject: false, timeout: 5000 });
|
|
70
70
|
const launchctlOutput = `${launchctl.stdout || ''}\n${launchctl.stderr || ''}`.toLowerCase();
|
|
71
71
|
const launchctlMatch = launchctlOutput.match(/"com\.openssh\.sshd"\s*=>\s*(enabled|disabled)/);
|
|
72
72
|
if (launchctlMatch) {
|
|
@@ -78,7 +78,7 @@ async function checkSshService() {
|
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
const result = await
|
|
81
|
+
const result = await execa('systemsetup', ['-getremotelogin'], { reject: false, timeout: 5000 });
|
|
82
82
|
const output = result.stdout || result.stderr || '';
|
|
83
83
|
if (/on/i.test(output)) return { status: 'pass', detail: 'Remote Login is on' };
|
|
84
84
|
if (/off/i.test(output)) {
|
|
@@ -96,8 +96,8 @@ async function checkSshService() {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
if (osInfo.isLinux) {
|
|
99
|
-
const ssh = await
|
|
100
|
-
const sshd = ssh.exitCode === 0 ? ssh : await
|
|
99
|
+
const ssh = await execa('systemctl', ['is-active', 'ssh'], { reject: false, timeout: 5000 });
|
|
100
|
+
const sshd = ssh.exitCode === 0 ? ssh : await execa('systemctl', ['is-active', 'sshd'], { reject: false, timeout: 5000 });
|
|
101
101
|
if (sshd.exitCode === 0) return { status: 'pass', detail: 'SSH service is active' };
|
|
102
102
|
return {
|
|
103
103
|
status: 'warn',
|
|
@@ -107,7 +107,7 @@ async function checkSshService() {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
if (osInfo.isWindows) {
|
|
110
|
-
const result = await
|
|
110
|
+
const result = await execa('sc', ['query', 'sshd'], { reject: false, timeout: 5000 });
|
|
111
111
|
if (/RUNNING/i.test(result.stdout)) return { status: 'pass', detail: 'OpenSSH Server is running' };
|
|
112
112
|
return {
|
|
113
113
|
status: 'warn',
|
|
@@ -196,7 +196,11 @@ function checkAiSafety() {
|
|
|
196
196
|
}
|
|
197
197
|
|
|
198
198
|
async function runProjectSelfTest(label, command) {
|
|
199
|
-
|
|
199
|
+
// Prefer splitting simple commands into args to avoid shell interpolation
|
|
200
|
+
const parts = String(command).split(' ').filter(Boolean);
|
|
201
|
+
const cmd = parts.shift();
|
|
202
|
+
const args = parts;
|
|
203
|
+
const result = await execa(cmd, args, {
|
|
200
204
|
reject: false,
|
|
201
205
|
timeout: 30000,
|
|
202
206
|
maxBuffer: 1024 * 1024,
|