@miraj181/ipingyou 2.1.9 → 2.1.18
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 +9 -4
- package/SECURITY.md +21 -0
- package/package.json +13 -13
- package/src/cli.js +55 -22
- package/src/lib/ai/safety.js +69 -12
- package/src/lib/broker.js +5 -5
- package/src/lib/chat.js +20 -6
- package/src/lib/checksum.js +22 -2
- package/src/lib/cleanup.js +60 -93
- package/src/lib/crypto.js +27 -0
- package/src/lib/open-url.js +28 -0
- package/src/lib/platform.js +38 -481
- package/src/lib/secure-print.js +7 -1
- package/src/lib/session-log.js +78 -3
- package/src/lib/socket-firewall.js +34 -0
- package/src/lib/ssh.js +32 -6
- package/src/lib/tunnel.js +1 -0
- package/src/lib/uid.js +6 -3
- package/src/lib/worker-runtime.js +81 -0
- package/src/lib/workers/crypto-checksum-worker.js +70 -0
- package/src/modes/ai.js +104 -31
- package/src/modes/client.js +20 -13
- package/src/modes/host.js +316 -116
- package/src/server.js +95 -18
package/src/lib/platform.js
CHANGED
|
@@ -1,29 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ============================================================
|
|
3
|
-
* Platform Detection & Dependency Auto-Bootstrapper
|
|
4
|
-
* ============================================================
|
|
5
|
-
* Detects OS, ensures download tools exist, then auto-installs
|
|
6
|
-
* ssh/cloudflared if missing. Bootstraps curl/wget first if
|
|
7
|
-
* even those are absent.
|
|
8
|
-
* ============================================================
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
1
|
import { execa } from 'execa';
|
|
12
2
|
import chalk from 'chalk';
|
|
13
|
-
import ora from 'ora';
|
|
14
3
|
import os from 'node:os';
|
|
15
4
|
import fs from 'node:fs';
|
|
16
|
-
import { createWriteStream } from 'node:fs';
|
|
17
|
-
import { pipeline } from 'node:stream/promises';
|
|
18
|
-
import { chmod, mkdir, stat, rename } from 'node:fs/promises';
|
|
19
|
-
import { join } from 'node:path';
|
|
20
|
-
|
|
21
|
-
// ─── OS Detection ────────────────────────────────────────────
|
|
22
5
|
|
|
23
|
-
/**
|
|
24
|
-
* Detect the current operating system.
|
|
25
|
-
* @returns {{ platform: string, isLinux: boolean, isMac: boolean, isWindows: boolean, distro: string|null, arch: string, hostname: string }}
|
|
26
|
-
*/
|
|
27
6
|
export function detectOS() {
|
|
28
7
|
const platform = process.platform;
|
|
29
8
|
return {
|
|
@@ -37,497 +16,75 @@ export function detectOS() {
|
|
|
37
16
|
};
|
|
38
17
|
}
|
|
39
18
|
|
|
40
|
-
/**
|
|
41
|
-
* Detect Linux distribution family.
|
|
42
|
-
* @returns {Promise<string>} 'debian' | 'arch' | 'fedora' | 'unknown'
|
|
43
|
-
*/
|
|
44
19
|
export async function detectLinuxDistro() {
|
|
45
20
|
try {
|
|
46
|
-
// Read the os-release file directly instead of spawning a process
|
|
47
21
|
const data = await fs.promises.readFile('/etc/os-release', 'utf8');
|
|
48
22
|
const lower = data.toLowerCase();
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (lower.includes('arch') || lower.includes('manjaro')) {
|
|
53
|
-
return 'arch';
|
|
54
|
-
}
|
|
55
|
-
if (lower.includes('fedora') || lower.includes('centos') || lower.includes('rhel')) {
|
|
56
|
-
return 'fedora';
|
|
57
|
-
}
|
|
58
|
-
} catch { /* ignore */ }
|
|
59
|
-
return 'unknown';
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// ─── Utility Helpers ─────────────────────────────────────────
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Check if a command exists on PATH.
|
|
66
|
-
* @param {string} cmd
|
|
67
|
-
* @returns {Promise<boolean>}
|
|
68
|
-
*/
|
|
69
|
-
export async function commandExists(cmd) {
|
|
70
|
-
try {
|
|
71
|
-
// Prefer direct exec of the check command to avoid shell parsing
|
|
72
|
-
if (process.platform === 'win32') {
|
|
73
|
-
await execa('where', [cmd], { reject: true });
|
|
74
|
-
} else {
|
|
75
|
-
await execa('which', [cmd], { reject: true });
|
|
76
|
-
}
|
|
77
|
-
return true;
|
|
23
|
+
if (/(ubuntu|debian|kali|mint)/.test(lower)) return 'debian';
|
|
24
|
+
if (/(arch|manjaro)/.test(lower)) return 'arch';
|
|
25
|
+
if (/(fedora|centos|rhel)/.test(lower)) return 'fedora';
|
|
78
26
|
} catch {
|
|
79
|
-
|
|
27
|
+
// Manual instructions fall back to the generic Linux guidance.
|
|
80
28
|
}
|
|
29
|
+
return 'unknown';
|
|
81
30
|
}
|
|
82
31
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
* @returns {Promise<boolean>}
|
|
86
|
-
*/
|
|
87
|
-
export async function hasSudo() {
|
|
88
|
-
if (process.platform === 'win32') return false;
|
|
89
|
-
return commandExists('sudo');
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Run a shell command with a spinner.
|
|
94
|
-
* @param {string} label
|
|
95
|
-
* @param {string} cmd
|
|
96
|
-
* @param {object} [opts]
|
|
97
|
-
* @returns {Promise<boolean>}
|
|
98
|
-
*/
|
|
99
|
-
async function runWithSpinner(label, cmd, opts = {}) {
|
|
100
|
-
const spinner = ora(label).start();
|
|
32
|
+
export async function commandExists(command) {
|
|
33
|
+
if (!/^[a-zA-Z0-9._+-]{1,64}$/.test(String(command || ''))) return false;
|
|
101
34
|
try {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
await execa('cmd', ['/c', cmd], { stdio: 'pipe', reject: true, ...opts });
|
|
109
|
-
} else {
|
|
110
|
-
await execa('sh', ['-c', cmd], { stdio: 'pipe', reject: true, ...opts });
|
|
111
|
-
}
|
|
112
|
-
} else {
|
|
113
|
-
throw new Error('Unsupported command type');
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
spinner.succeed(label.replace('...', '') + chalk.green(' ✓'));
|
|
35
|
+
const probe = process.platform === 'win32' ? ['where', command] : ['which', command];
|
|
36
|
+
await execa(probe[0], [probe[1]], {
|
|
37
|
+
reject: true,
|
|
38
|
+
timeout: 5000,
|
|
39
|
+
maxBuffer: 64 * 1024,
|
|
40
|
+
});
|
|
117
41
|
return true;
|
|
118
|
-
} catch
|
|
119
|
-
spinner.fail(label.replace('...', '') + chalk.red(` ✗ ${err.shortMessage || err.message}`));
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Resolve the CPU architecture for download URLs.
|
|
126
|
-
* @returns {string}
|
|
127
|
-
*/
|
|
128
|
-
function resolveArch() {
|
|
129
|
-
const arch = os.arch();
|
|
130
|
-
switch (arch) {
|
|
131
|
-
case 'x64': return 'amd64';
|
|
132
|
-
case 'arm64': return 'arm64';
|
|
133
|
-
case 'arm': return 'arm';
|
|
134
|
-
default: return arch;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ─── Download Tool Bootstrap ─────────────────────────────────
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Find the first available download tool.
|
|
142
|
-
* @returns {Promise<string|null>} 'curl' | 'wget' | 'powershell' | null
|
|
143
|
-
*/
|
|
144
|
-
async function findDownloader() {
|
|
145
|
-
for (const tool of ['curl', 'wget']) {
|
|
146
|
-
if (await commandExists(tool)) return tool;
|
|
147
|
-
}
|
|
148
|
-
// Windows fallback: PowerShell is almost always present
|
|
149
|
-
if (process.platform === 'win32') {
|
|
150
|
-
if (await commandExists('powershell')) return 'powershell';
|
|
151
|
-
}
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Ensure at least one download tool (curl/wget) is installed.
|
|
157
|
-
* If none exist, install curl using the system package manager.
|
|
158
|
-
* @returns {Promise<string>} The download tool name that is now available.
|
|
159
|
-
* @throws {Error} If no downloader can be provisioned.
|
|
160
|
-
*/
|
|
161
|
-
async function ensureDownloader() {
|
|
162
|
-
let tool = await findDownloader();
|
|
163
|
-
if (tool) return tool;
|
|
164
|
-
|
|
165
|
-
console.log(chalk.yellow('\n ⚠️ No download tool found (curl, wget). Bootstrapping curl...\n'));
|
|
166
|
-
|
|
167
|
-
const osInfo = detectOS();
|
|
168
|
-
|
|
169
|
-
if (osInfo.isLinux) {
|
|
170
|
-
const distro = await detectLinuxDistro();
|
|
171
|
-
const sudo = (await hasSudo()) ? 'sudo ' : '';
|
|
172
|
-
|
|
173
|
-
const installCmds = {
|
|
174
|
-
debian: `${sudo}apt-get update -qq && ${sudo}apt-get install -y curl`,
|
|
175
|
-
arch: `${sudo}pacman -Sy --noconfirm curl`,
|
|
176
|
-
fedora: `${sudo}dnf install -y curl`,
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
const cmd = installCmds[distro];
|
|
180
|
-
if (cmd) {
|
|
181
|
-
const ok = await runWithSpinner(` Installing ${chalk.cyan('curl')} via ${distro} package manager...`, cmd);
|
|
182
|
-
if (ok) return 'curl';
|
|
183
|
-
}
|
|
184
|
-
} else if (osInfo.isMac) {
|
|
185
|
-
// macOS: curl ships with Xcode CLT. If truly missing, try xcode-select.
|
|
186
|
-
const ok = await runWithSpinner(
|
|
187
|
-
` Installing ${chalk.cyan('Xcode Command Line Tools')} (includes curl)...`,
|
|
188
|
-
'xcode-select --install'
|
|
189
|
-
);
|
|
190
|
-
if (ok || await commandExists('curl')) return 'curl';
|
|
191
|
-
} else if (osInfo.isWindows) {
|
|
192
|
-
// Windows: try winget to install curl
|
|
193
|
-
if (await commandExists('winget')) {
|
|
194
|
-
const ok = await runWithSpinner(
|
|
195
|
-
` Installing ${chalk.cyan('curl')} via winget...`,
|
|
196
|
-
'winget install --id cURL.cURL --accept-source-agreements --accept-package-agreements -e'
|
|
197
|
-
);
|
|
198
|
-
if (ok) return 'curl';
|
|
199
|
-
}
|
|
200
|
-
// PowerShell fallback is always available on modern Windows
|
|
201
|
-
if (await commandExists('powershell')) return 'powershell';
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
throw new Error(
|
|
205
|
-
'Could not provision a download tool (curl/wget). Please install curl manually and retry.'
|
|
206
|
-
);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// ─── Download Helpers ────────────────────────────────────────
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Download a file using whichever tool is available.
|
|
213
|
-
* @param {string} url
|
|
214
|
-
* @param {string} destPath
|
|
215
|
-
* @param {string} downloader 'curl' | 'wget' | 'powershell'
|
|
216
|
-
* @returns {Promise<boolean>}
|
|
217
|
-
*/
|
|
218
|
-
async function downloadFile(url, destPath, downloader) {
|
|
219
|
-
const cmds = {
|
|
220
|
-
curl: ['curl', ['-fsSL', '-o', destPath, url]],
|
|
221
|
-
wget: ['wget', ['-q', '-O', destPath, url]],
|
|
222
|
-
powershell: ['powershell', ["-Command", `Invoke-WebRequest -Uri '${url}' -OutFile '${destPath}'`]],
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
const entry = cmds[downloader];
|
|
226
|
-
if (!entry) return false;
|
|
227
|
-
const [bin, args] = entry;
|
|
228
|
-
|
|
229
|
-
return runWithSpinner(
|
|
230
|
-
` Downloading ${chalk.cyan(url.split('/').pop())}...`,
|
|
231
|
-
[bin, ...args]
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// ─── Cloudflared Installer ───────────────────────────────────
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Build the cloudflared download URL for the current platform.
|
|
239
|
-
* @param {{ isLinux: boolean, isMac: boolean, isWindows: boolean }} osInfo
|
|
240
|
-
* @returns {{ url: string, filename: string }|null}
|
|
241
|
-
*/
|
|
242
|
-
function cloudflaredDownloadUrl(osInfo) {
|
|
243
|
-
const arch = resolveArch();
|
|
244
|
-
const base = 'https://github.com/cloudflare/cloudflared/releases/latest/download';
|
|
245
|
-
|
|
246
|
-
if (osInfo.isLinux) {
|
|
247
|
-
return { url: `${base}/cloudflared-linux-${arch}`, filename: 'cloudflared' };
|
|
248
|
-
}
|
|
249
|
-
if (osInfo.isMac) {
|
|
250
|
-
// Homebrew is preferred on macOS — but provide direct download as fallback
|
|
251
|
-
return { url: `${base}/cloudflared-darwin-${arch}.tgz`, filename: 'cloudflared-darwin.tgz' };
|
|
252
|
-
}
|
|
253
|
-
if (osInfo.isWindows) {
|
|
254
|
-
return { url: `${base}/cloudflared-windows-${arch}.exe`, filename: 'cloudflared.exe' };
|
|
255
|
-
}
|
|
256
|
-
return null;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Install cloudflared binary from official GitHub releases.
|
|
261
|
-
* @param {{ isLinux: boolean, isMac: boolean, isWindows: boolean }} osInfo
|
|
262
|
-
* @param {string} downloader
|
|
263
|
-
* @returns {Promise<boolean>}
|
|
264
|
-
*/
|
|
265
|
-
async function installCloudflared(osInfo, downloader) {
|
|
266
|
-
// ── macOS: prefer Homebrew ─────────────────────────────────
|
|
267
|
-
if (osInfo.isMac && await commandExists('brew')) {
|
|
268
|
-
console.log(chalk.yellow(' 🍺 Installing cloudflared via Homebrew...'));
|
|
269
|
-
return runWithSpinner(
|
|
270
|
-
` ${chalk.cyan('brew install cloudflared')}...`,
|
|
271
|
-
'brew install cloudflared'
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// ── Linux: try native package managers first ───────────────
|
|
276
|
-
if (osInfo.isLinux) {
|
|
277
|
-
const distro = await detectLinuxDistro();
|
|
278
|
-
const sudo = (await hasSudo()) ? 'sudo ' : '';
|
|
279
|
-
|
|
280
|
-
// Debian/Ubuntu: official Cloudflare apt repo
|
|
281
|
-
if (distro === 'debian') {
|
|
282
|
-
const aptOk = await runWithSpinner(
|
|
283
|
-
` Adding Cloudflare APT repo & installing ${chalk.cyan('cloudflared')}...`,
|
|
284
|
-
[
|
|
285
|
-
`${sudo}mkdir -p --mode=0755 /usr/share/keyrings`,
|
|
286
|
-
`curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | ${sudo}tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null`,
|
|
287
|
-
`echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" | ${sudo}tee /etc/apt/sources.list.d/cloudflared.list`,
|
|
288
|
-
`${sudo}apt-get update -qq`,
|
|
289
|
-
`${sudo}apt-get install -y cloudflared`,
|
|
290
|
-
].join(' && ')
|
|
291
|
-
);
|
|
292
|
-
if (aptOk) return true;
|
|
293
|
-
// Fall through to binary download if apt method fails
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// ── Windows: try winget first ──────────────────────────────
|
|
298
|
-
if (osInfo.isWindows && await commandExists('winget')) {
|
|
299
|
-
const ok = await runWithSpinner(
|
|
300
|
-
` Installing ${chalk.cyan('cloudflared')} via winget...`,
|
|
301
|
-
'winget install --id Cloudflare.cloudflared --accept-source-agreements --accept-package-agreements -e'
|
|
302
|
-
);
|
|
303
|
-
if (ok) return true;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// ── Fallback: direct binary download ───────────────────────
|
|
307
|
-
const dl = cloudflaredDownloadUrl(osInfo);
|
|
308
|
-
if (!dl) {
|
|
309
|
-
console.log(chalk.red(' ✗ Unsupported platform for cloudflared auto-install.'));
|
|
42
|
+
} catch {
|
|
310
43
|
return false;
|
|
311
44
|
}
|
|
312
|
-
|
|
313
|
-
const tmpDir = join(os.tmpdir(), 'ipingyou-bootstrap');
|
|
314
|
-
try { await mkdir(tmpDir, { recursive: true }); } catch { /* exists */ }
|
|
315
|
-
const destPath = join(tmpDir, dl.filename);
|
|
316
|
-
|
|
317
|
-
const ok = await downloadFile(dl.url, destPath, downloader);
|
|
318
|
-
if (!ok) return false;
|
|
319
|
-
|
|
320
|
-
// Handle macOS .tgz extraction
|
|
321
|
-
if (dl.filename.endsWith('.tgz')) {
|
|
322
|
-
const extractOk = await runWithSpinner(
|
|
323
|
-
` Extracting ${chalk.cyan('cloudflared')} archive...`,
|
|
324
|
-
`tar -xzf "${destPath}" -C "${tmpDir}"`
|
|
325
|
-
);
|
|
326
|
-
if (!extractOk) return false;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Move binary to a PATH location
|
|
330
|
-
if (osInfo.isLinux || osInfo.isMac) {
|
|
331
|
-
const binaryPath = dl.filename.endsWith('.tgz')
|
|
332
|
-
? join(tmpDir, 'cloudflared')
|
|
333
|
-
: destPath;
|
|
334
|
-
|
|
335
|
-
try { await chmod(binaryPath, 0o755); } catch { /* ignore */ }
|
|
336
|
-
|
|
337
|
-
const sudo = (await hasSudo()) ? 'sudo ' : '';
|
|
338
|
-
const installPath = '/usr/local/bin/cloudflared';
|
|
339
|
-
|
|
340
|
-
return runWithSpinner(
|
|
341
|
-
` Moving ${chalk.cyan('cloudflared')} to ${chalk.dim(installPath)}...`,
|
|
342
|
-
`${sudo}mv "${binaryPath}" "${installPath}"`
|
|
343
|
-
);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (osInfo.isWindows) {
|
|
347
|
-
// Place in user's local app data
|
|
348
|
-
const winDir = join(os.homedir(), 'AppData', 'Local', 'cloudflared');
|
|
349
|
-
try { await mkdir(winDir, { recursive: true }); } catch { /* exists */ }
|
|
350
|
-
const finalPath = join(winDir, 'cloudflared.exe');
|
|
351
|
-
|
|
352
|
-
try {
|
|
353
|
-
await rename(destPath, finalPath);
|
|
354
|
-
console.log(chalk.green(` ✓ cloudflared installed at ${chalk.dim(finalPath)}`));
|
|
355
|
-
console.log(chalk.yellow(` ⚠️ Add ${chalk.dim(winDir)} to your PATH to use cloudflared globally.`));
|
|
356
|
-
return true;
|
|
357
|
-
} catch (err) {
|
|
358
|
-
console.log(chalk.red(` ✗ Failed to move cloudflared: ${err.message}`));
|
|
359
|
-
return false;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
return false;
|
|
364
45
|
}
|
|
365
46
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
* @returns {Promise<boolean>}
|
|
372
|
-
*/
|
|
373
|
-
async function installOpenSSH(osInfo) {
|
|
374
|
-
if (osInfo.isMac) {
|
|
375
|
-
// macOS ships with ssh; if missing, it means Xcode CLT isn't installed
|
|
376
|
-
console.log(chalk.dim(' ℹ️ macOS ships with SSH by default.'));
|
|
377
|
-
console.log(chalk.dim(' If missing, enable in: System Preferences → Sharing → Remote Login'));
|
|
378
|
-
// Try xcode-select as last resort
|
|
379
|
-
if (!(await commandExists('ssh'))) {
|
|
380
|
-
return runWithSpinner(
|
|
381
|
-
` Installing ${chalk.cyan('Xcode Command Line Tools')} (includes ssh)...`,
|
|
382
|
-
'xcode-select --install'
|
|
383
|
-
);
|
|
384
|
-
}
|
|
385
|
-
return true;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (osInfo.isLinux) {
|
|
389
|
-
const distro = await detectLinuxDistro();
|
|
390
|
-
const sudo = (await hasSudo()) ? 'sudo ' : '';
|
|
391
|
-
|
|
392
|
-
const cmds = {
|
|
393
|
-
debian: `${sudo}apt-get update -qq && ${sudo}apt-get install -y openssh-client openssh-server`,
|
|
394
|
-
arch: `${sudo}pacman -Sy --noconfirm openssh`,
|
|
395
|
-
fedora: `${sudo}dnf install -y openssh-clients openssh-server`,
|
|
396
|
-
};
|
|
397
|
-
|
|
398
|
-
const cmd = cmds[distro];
|
|
399
|
-
if (cmd) {
|
|
400
|
-
return runWithSpinner(` Installing ${chalk.cyan('openssh')} via ${distro} package manager...`, cmd);
|
|
401
|
-
}
|
|
47
|
+
function printInstallGuidance(osInfo, missing) {
|
|
48
|
+
console.log(chalk.yellow(' ⚠️ Required system tools are missing.'));
|
|
49
|
+
console.log(chalk.dim(' iPingYou does not download or execute native installers automatically.'));
|
|
50
|
+
console.log(chalk.dim(' Install the tools through your trusted OS package manager, then rerun iPingYou.'));
|
|
51
|
+
console.log('');
|
|
402
52
|
|
|
403
|
-
|
|
404
|
-
|
|
53
|
+
if (missing.includes('cloudflared')) {
|
|
54
|
+
if (osInfo.isMac) console.log(chalk.cyan(' brew install cloudflared'));
|
|
55
|
+
else if (osInfo.isWindows) console.log(chalk.cyan(' winget install --id Cloudflare.cloudflared -e'));
|
|
56
|
+
else console.log(chalk.cyan(' Follow: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/'));
|
|
405
57
|
}
|
|
406
58
|
|
|
407
|
-
if (
|
|
408
|
-
|
|
409
|
-
if (
|
|
410
|
-
|
|
411
|
-
` Installing ${chalk.cyan('OpenSSH')} via winget...`,
|
|
412
|
-
'winget install --id Microsoft.OpenSSH.Client --accept-source-agreements --accept-package-agreements -e'
|
|
413
|
-
);
|
|
414
|
-
if (ok) return true;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Fallback: PowerShell Add-WindowsCapability
|
|
418
|
-
if (await commandExists('powershell')) {
|
|
419
|
-
const ok = await runWithSpinner(
|
|
420
|
-
` Installing ${chalk.cyan('OpenSSH')} via PowerShell...`,
|
|
421
|
-
'powershell -Command "Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0"'
|
|
422
|
-
);
|
|
423
|
-
if (ok) return true;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
console.log(chalk.yellow(' ⚠️ Could not auto-install OpenSSH on Windows.'));
|
|
427
|
-
console.log(chalk.dim(' Manual: Settings → Apps → Optional Features → OpenSSH Client'));
|
|
428
|
-
return false;
|
|
59
|
+
if (missing.includes('ssh')) {
|
|
60
|
+
if (osInfo.isMac) console.log(chalk.cyan(' Enable Remote Login in System Settings → General → Sharing'));
|
|
61
|
+
else if (osInfo.isWindows) console.log(chalk.cyan(' Add OpenSSH Client/Server from Windows Optional Features'));
|
|
62
|
+
else console.log(chalk.cyan(' Debian/Ubuntu: sudo apt-get install openssh-client openssh-server'));
|
|
429
63
|
}
|
|
430
|
-
|
|
431
|
-
return false;
|
|
64
|
+
console.log('');
|
|
432
65
|
}
|
|
433
66
|
|
|
434
|
-
// ─── Main Dependency Check ───────────────────────────────────
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* Run full dependency check with auto-bootstrap.
|
|
438
|
-
*
|
|
439
|
-
* Pipeline:
|
|
440
|
-
* 1. Detect OS
|
|
441
|
-
* 2. Check ssh + cloudflared
|
|
442
|
-
* 3. If anything is missing → ensure a download tool exists (bootstrap curl if needed)
|
|
443
|
-
* 4. Auto-install missing deps using the best method per platform
|
|
444
|
-
*
|
|
445
|
-
* @returns {Promise<{ ssh: boolean, cloudflared: boolean }>}
|
|
446
|
-
*/
|
|
447
67
|
export async function checkDependencies() {
|
|
448
68
|
const osInfo = detectOS();
|
|
449
|
-
const results = {
|
|
69
|
+
const results = {
|
|
70
|
+
ssh: await commandExists('ssh'),
|
|
71
|
+
cloudflared: await commandExists('cloudflared'),
|
|
72
|
+
};
|
|
450
73
|
|
|
451
74
|
console.log('');
|
|
452
75
|
console.log(chalk.bold(' 🔍 Dependency Check'));
|
|
453
76
|
console.log(chalk.dim(' ─────────────────────────────────'));
|
|
454
|
-
|
|
455
|
-
// ── Step 1: Probe for existing binaries ─────────────────────
|
|
456
|
-
results.ssh = await commandExists('ssh');
|
|
457
77
|
console.log(` ${results.ssh ? chalk.green('✓') : chalk.red('✗')} ssh ${results.ssh ? chalk.dim('found') : chalk.red('missing')}`);
|
|
458
|
-
|
|
459
|
-
results.cloudflared = await commandExists('cloudflared');
|
|
460
78
|
console.log(` ${results.cloudflared ? chalk.green('✓') : chalk.red('✗')} cloudflared ${results.cloudflared ? chalk.dim('found') : chalk.red('missing')}`);
|
|
461
|
-
|
|
462
79
|
console.log('');
|
|
463
80
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
// ── Step 3: Bootstrap a download tool ───────────────────────
|
|
471
|
-
let downloader;
|
|
472
|
-
try {
|
|
473
|
-
console.log(chalk.bold(' 🔧 Bootstrapping download tools...'));
|
|
474
|
-
|
|
475
|
-
// Check what download tools exist
|
|
476
|
-
const hasCurl = await commandExists('curl');
|
|
477
|
-
const hasWget = await commandExists('wget');
|
|
478
|
-
const hasWinget = osInfo.isWindows ? await commandExists('winget') : false;
|
|
479
|
-
|
|
480
|
-
console.log(` ${hasCurl ? chalk.green('✓') : chalk.red('✗')} curl ${hasCurl ? chalk.dim('found') : chalk.red('missing')}`);
|
|
481
|
-
console.log(` ${hasWget ? chalk.green('✓') : chalk.red('✗')} wget ${hasWget ? chalk.dim('found') : chalk.red('missing')}`);
|
|
482
|
-
if (osInfo.isWindows) {
|
|
483
|
-
console.log(` ${hasWinget ? chalk.green('✓') : chalk.red('✗')} winget ${hasWinget ? chalk.dim('found') : chalk.red('missing')}`);
|
|
484
|
-
}
|
|
485
|
-
console.log('');
|
|
486
|
-
|
|
487
|
-
downloader = await ensureDownloader();
|
|
488
|
-
console.log(chalk.green(` ✓ Using ${chalk.cyan(downloader)} as download tool\n`));
|
|
489
|
-
} catch (err) {
|
|
490
|
-
console.log(chalk.red(`\n ✗ ${err.message}`));
|
|
491
|
-
console.log(chalk.dim(' Cannot proceed with auto-installation.\n'));
|
|
492
|
-
return results;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// ── Step 4: Install missing dependencies ────────────────────
|
|
496
|
-
console.log(chalk.bold(' ⚡ Auto-installing missing dependencies...\n'));
|
|
497
|
-
|
|
498
|
-
if (!results.ssh) {
|
|
499
|
-
const installed = await installOpenSSH(osInfo);
|
|
500
|
-
results.ssh = installed || (await commandExists('ssh'));
|
|
501
|
-
console.log('');
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
if (!results.cloudflared) {
|
|
505
|
-
const installed = await installCloudflared(osInfo, downloader);
|
|
506
|
-
results.cloudflared = installed || (await commandExists('cloudflared'));
|
|
507
|
-
console.log('');
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// ── Final report ────────────────────────────────────────────
|
|
511
|
-
console.log(chalk.dim(' ─────────────────────────────────'));
|
|
512
|
-
console.log(chalk.bold(' 📋 Final Status'));
|
|
513
|
-
console.log(` ${results.ssh ? chalk.green('✓') : chalk.red('✗')} ssh ${results.ssh ? chalk.green('ready') : chalk.red('unavailable')}`);
|
|
514
|
-
console.log(` ${results.cloudflared ? chalk.green('✓') : chalk.red('✗')} cloudflared ${results.cloudflared ? chalk.green('ready') : chalk.red('unavailable')}`);
|
|
515
|
-
|
|
516
|
-
if (!results.ssh || !results.cloudflared) {
|
|
517
|
-
console.log('');
|
|
518
|
-
console.log(chalk.yellow(' ⚠️ Some dependencies could not be installed automatically.'));
|
|
519
|
-
console.log(chalk.dim(' Please install them manually and re-run the tool.'));
|
|
520
|
-
|
|
521
|
-
if (!results.cloudflared) {
|
|
522
|
-
console.log(chalk.dim(' cloudflared → https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/'));
|
|
523
|
-
}
|
|
524
|
-
if (!results.ssh) {
|
|
525
|
-
console.log(chalk.dim(' openssh → https://www.openssh.com/portable.html'));
|
|
526
|
-
}
|
|
81
|
+
const missing = Object.entries(results)
|
|
82
|
+
.filter(([, available]) => !available)
|
|
83
|
+
.map(([name]) => name);
|
|
84
|
+
if (missing.length > 0) {
|
|
85
|
+
printInstallGuidance(osInfo, missing);
|
|
527
86
|
} else {
|
|
528
|
-
console.log(chalk.green('
|
|
87
|
+
console.log(chalk.green(' ✅ All dependencies satisfied!\n'));
|
|
529
88
|
}
|
|
530
|
-
|
|
531
|
-
console.log('');
|
|
532
89
|
return results;
|
|
533
90
|
}
|
package/src/lib/secure-print.js
CHANGED
|
@@ -18,6 +18,8 @@ import os from 'node:os';
|
|
|
18
18
|
// Keyed secret for deterministic, non-reversible log masking tokens.
|
|
19
19
|
// Can be overridden for stable cross-process correlation if needed.
|
|
20
20
|
const MASKING_KEY = process.env.SECURE_PRINT_MASK_KEY || crypto.randomBytes(32).toString('hex');
|
|
21
|
+
let lastMaskedFingerprint = null;
|
|
22
|
+
let lastMaskedResult = null;
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* Verify the current process is being run by the same OS user interactively.
|
|
@@ -47,10 +49,14 @@ function isVerifiedHostUser() {
|
|
|
47
49
|
*/
|
|
48
50
|
function maskSensitive(value) {
|
|
49
51
|
const normalized = String(value);
|
|
52
|
+
const fingerprint = crypto.createHash('sha256').update(normalized).digest('hex');
|
|
53
|
+
if (fingerprint === lastMaskedFingerprint) return lastMaskedResult;
|
|
50
54
|
const hash = crypto
|
|
51
55
|
.pbkdf2Sync(MASKING_KEY, normalized, 210000, 32, 'sha256')
|
|
52
56
|
.toString('hex');
|
|
53
|
-
|
|
57
|
+
lastMaskedFingerprint = fingerprint;
|
|
58
|
+
lastMaskedResult = `[pbkdf2:${hash.slice(0, 12)}…]`;
|
|
59
|
+
return lastMaskedResult;
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
/**
|