@miraj181/ipingyou 2.1.22 → 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/package.json +1 -1
- package/src/lib/client/broker.js +7 -7
- package/src/lib/services/platform.js +31 -75
- package/src/lib/services/ssh.js +14 -1
- package/src/lib/services/tunnel.js +3 -1
- package/src/modes/client.js +14 -1
- package/src/modes/doctor.js +3 -2
- package/src/modes/host.js +23 -8
package/package.json
CHANGED
package/src/lib/client/broker.js
CHANGED
|
@@ -216,20 +216,20 @@ export async function resolveUID(brokerUrl, uid, password, silent = false, reque
|
|
|
216
216
|
try {
|
|
217
217
|
let decPassword = password;
|
|
218
218
|
if (data.isClientSpecific) {
|
|
219
|
+
// Key derivation must match host side exactly:
|
|
220
|
+
// [password, broker-observed-IP, uid].join('|')
|
|
219
221
|
const clientKeySalt = [
|
|
220
222
|
password,
|
|
221
223
|
data.ip || 'unknown',
|
|
222
|
-
|
|
223
|
-
os.hostname(),
|
|
224
|
-
`${os.type()} ${os.release()} (${os.arch()})`
|
|
224
|
+
uid
|
|
225
225
|
].join('|');
|
|
226
226
|
decPassword = crypto.createHash('sha256').update(clientKeySalt).digest('hex');
|
|
227
227
|
}
|
|
228
228
|
decryptedPayload = await decryptAsync(data.iv, data.ciphertext, decPassword, data.salt);
|
|
229
|
-
} catch {
|
|
230
|
-
if (spinner) spinner.fail(
|
|
231
|
-
if (!spinner) console.error(chalk.red(
|
|
232
|
-
logSessionEvent('broker_decrypt_failed', { uid }, 'warn');
|
|
229
|
+
} catch (decErr) {
|
|
230
|
+
if (spinner) spinner.fail(`Decryption failed — ${decErr.message}`);
|
|
231
|
+
if (!spinner) console.error(chalk.red(` ❌ Error: Could not decrypt tunnel data: ${decErr.message}`));
|
|
232
|
+
logSessionEvent('broker_decrypt_failed', { uid, error: decErr.message }, 'warn');
|
|
233
233
|
return null;
|
|
234
234
|
}
|
|
235
235
|
|
|
@@ -225,92 +225,47 @@ async function autoInstallDependency(dep, osInfo) {
|
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
227
|
} else if (dep === 'cloudflared') {
|
|
228
|
+
const localBinDir = path.join(os.homedir(), '.ipingyou', 'bin');
|
|
229
|
+
await fs.promises.mkdir(localBinDir, { recursive: true, mode: 0o700 });
|
|
230
|
+
const localPath = path.join(localBinDir, osInfo.isWindows ? 'cloudflared.exe' : 'cloudflared');
|
|
231
|
+
|
|
228
232
|
if (osInfo.isMac) {
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const arch = osInfo.arch === 'arm64' ? 'arm64' : 'amd64';
|
|
235
|
-
const url = `https://github.com/cloudflare/cloudflared/${relLatest}/download/cloudflared-darwin-${arch}.tgz`;
|
|
236
|
-
const tgzPath = path.join(os.tmpdir(), 'cloudflared.tgz');
|
|
237
|
-
await downloadUrlToPath(url, tgzPath);
|
|
238
|
-
await executeWithRetry('tar', ['-xzf', tgzPath, '-C', os.tmpdir()]);
|
|
239
|
-
await executeWithRetry('sudo', ['cp', path.join(os.tmpdir(), 'cloudflared'), '/usr/local/bin/cloudflared']);
|
|
240
|
-
await executeWithRetry('sudo', ['chmod', '+x', '/usr/local/bin/cloudflared']);
|
|
241
|
-
}
|
|
233
|
+
const arch = osInfo.arch === 'arm64' ? 'arm64' : 'amd64';
|
|
234
|
+
const url = `https://github.com/cloudflare/cloudflared/${relLatest}/download/cloudflared-darwin-${arch}.tgz`;
|
|
235
|
+
const tgzPath = path.join(os.tmpdir(), 'cloudflared.tgz');
|
|
236
|
+
await downloadUrlToPath(url, tgzPath);
|
|
237
|
+
await execa('tar', ['-xzf', tgzPath, '-C', localBinDir]);
|
|
242
238
|
} else if (osInfo.isWindows) {
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
} else {
|
|
247
|
-
console.log(chalk.yellow(' winget not found. Downloading cloudflared executable directly...'));
|
|
248
|
-
const arch = osInfo.arch === 'x64' ? 'amd64' : '386';
|
|
249
|
-
const url = `https://github.com/cloudflare/cloudflared/${relLatest}/download/cloudflared-windows-${arch}.exe`;
|
|
250
|
-
const destDir = path.join(process.env.LOCALAPPDATA, 'Microsoft', 'WindowsApps');
|
|
251
|
-
await fs.promises.mkdir(destDir, { recursive: true });
|
|
252
|
-
const destPath = path.join(destDir, 'cloudflared.exe');
|
|
253
|
-
await downloadUrlToPath(url, destPath);
|
|
254
|
-
console.log(chalk.green(` Downloaded cloudflared to ${destPath}`));
|
|
255
|
-
}
|
|
239
|
+
const arch = osInfo.arch === 'x64' ? 'amd64' : '386';
|
|
240
|
+
const url = `https://github.com/cloudflare/cloudflared/${relLatest}/download/cloudflared-windows-${arch}.exe`;
|
|
241
|
+
await downloadUrlToPath(url, localPath);
|
|
256
242
|
} else if (osInfo.isLinux) {
|
|
257
|
-
const distro = await detectLinuxDistro();
|
|
258
243
|
const arch = osInfo.arch === 'x64' ? 'amd64' : (osInfo.arch === 'arm64' ? 'arm64' : '386');
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
try {
|
|
263
|
-
const url = `https://github.com/cloudflare/cloudflared/${relLatest}/download/cloudflared-linux-${arch}.deb`;
|
|
264
|
-
const debPath = path.join(os.tmpdir(), 'cloudflared.deb');
|
|
265
|
-
await downloadUrlToPath(url, debPath);
|
|
266
|
-
try {
|
|
267
|
-
await executeWithRetry('sudo', ['dpkg', '-i', debPath]);
|
|
268
|
-
} catch {
|
|
269
|
-
await executeWithRetry('sudo', ['apt-get', 'update']);
|
|
270
|
-
await executeWithRetry('sudo', ['apt-get', 'install', '-f', '-y']);
|
|
271
|
-
}
|
|
272
|
-
installed = true;
|
|
273
|
-
} catch (err) {
|
|
274
|
-
console.log(chalk.yellow(` Failed to install via deb package: ${err.message}. Trying direct binary...`));
|
|
275
|
-
}
|
|
276
|
-
} else if (distro === 'fedora' || (await commandExists('dnf')) || (await commandExists('yum'))) {
|
|
277
|
-
try {
|
|
278
|
-
const rpmArch = osInfo.arch === 'x64' ? 'x86_64' : (osInfo.arch === 'arm64' ? 'aarch64' : 'i386');
|
|
279
|
-
const url = `https://github.com/cloudflare/cloudflared/${relLatest}/download/cloudflared-linux-${rpmArch}.rpm`;
|
|
280
|
-
const rpmPath = path.join(os.tmpdir(), 'cloudflared.rpm');
|
|
281
|
-
await downloadUrlToPath(url, rpmPath);
|
|
282
|
-
const pkgManager = await commandExists('dnf') ? 'dnf' : 'yum';
|
|
283
|
-
await executeWithRetry('sudo', [pkgManager, 'install', '-y', rpmPath]);
|
|
284
|
-
installed = true;
|
|
285
|
-
} catch (err) {
|
|
286
|
-
console.log(chalk.yellow(` Failed to install via rpm package: ${err.message}. Trying direct binary...`));
|
|
287
|
-
}
|
|
288
|
-
} else if (distro === 'arch' || (await commandExists('pacman'))) {
|
|
289
|
-
try {
|
|
290
|
-
await executeWithRetry('sudo', ['pacman', '-S', '--noconfirm', 'cloudflared']);
|
|
291
|
-
installed = true;
|
|
292
|
-
} catch (err) {
|
|
293
|
-
console.log(chalk.yellow(` Failed to install via pacman: ${err.message}. Trying direct binary...`));
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (!installed) {
|
|
298
|
-
console.log(chalk.yellow(' Downloading cloudflared binary directly...'));
|
|
299
|
-
const url = `https://github.com/cloudflare/cloudflared/${relLatest}/download/cloudflared-linux-${arch}`;
|
|
300
|
-
const binPath = path.join(os.tmpdir(), 'cloudflared');
|
|
301
|
-
await downloadUrlToPath(url, binPath);
|
|
302
|
-
await executeWithRetry('sudo', ['cp', binPath, '/usr/local/bin/cloudflared']);
|
|
303
|
-
await executeWithRetry('sudo', ['chmod', '+x', '/usr/local/bin/cloudflared']);
|
|
304
|
-
}
|
|
244
|
+
const url = `https://github.com/cloudflare/cloudflared/${relLatest}/download/cloudflared-linux-${arch}`;
|
|
245
|
+
await downloadUrlToPath(url, localPath);
|
|
246
|
+
await fs.promises.chmod(localPath, 0o755);
|
|
305
247
|
}
|
|
306
248
|
}
|
|
307
249
|
}
|
|
308
250
|
|
|
251
|
+
export async function getCloudflaredPath() {
|
|
252
|
+
if (await commandExists('cloudflared')) {
|
|
253
|
+
return 'cloudflared';
|
|
254
|
+
}
|
|
255
|
+
const localBinDir = path.join(os.homedir(), '.ipingyou', 'bin');
|
|
256
|
+
const localPath = path.join(localBinDir, process.platform === 'win32' ? 'cloudflared.exe' : 'cloudflared');
|
|
257
|
+
if (fs.existsSync(localPath)) {
|
|
258
|
+
return localPath;
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
309
263
|
export async function checkDependencies() {
|
|
310
264
|
const osInfo = detectOS();
|
|
265
|
+
const cfPath = await getCloudflaredPath();
|
|
311
266
|
let results = {
|
|
312
267
|
ssh: await commandExists('ssh'),
|
|
313
|
-
cloudflared:
|
|
268
|
+
cloudflared: cfPath !== null,
|
|
314
269
|
};
|
|
315
270
|
|
|
316
271
|
const missing = Object.entries(results)
|
|
@@ -338,9 +293,10 @@ export async function checkDependencies() {
|
|
|
338
293
|
}
|
|
339
294
|
|
|
340
295
|
// Re-verify after installation
|
|
296
|
+
const finalCfPath = await getCloudflaredPath();
|
|
341
297
|
results = {
|
|
342
298
|
ssh: await commandExists('ssh'),
|
|
343
|
-
cloudflared:
|
|
299
|
+
cloudflared: finalCfPath !== null,
|
|
344
300
|
};
|
|
345
301
|
|
|
346
302
|
const stillMissing = Object.entries(results)
|
package/src/lib/services/ssh.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
function getCloudflaredPathSync() {
|
|
7
|
+
const localBinDir = path.join(os.homedir(), '.ipingyou', 'bin');
|
|
8
|
+
const localPath = path.join(localBinDir, process.platform === 'win32' ? 'cloudflared.exe' : 'cloudflared');
|
|
9
|
+
if (fs.existsSync(localPath)) {
|
|
10
|
+
return localPath;
|
|
11
|
+
}
|
|
12
|
+
return 'cloudflared';
|
|
13
|
+
}
|
|
2
14
|
|
|
3
15
|
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
16
|
|
|
@@ -58,7 +70,8 @@ export function getSshControlOptions(hostname) {
|
|
|
58
70
|
|
|
59
71
|
export function buildProxyCommandOption(hostname) {
|
|
60
72
|
const safeHostname = assertSafeHostname(hostname, 'tunnel hostname');
|
|
61
|
-
|
|
73
|
+
const cfExecutable = getCloudflaredPathSync();
|
|
74
|
+
return ['-o', `ProxyCommand=${cfExecutable} access tcp --hostname ${safeHostname}`];
|
|
62
75
|
}
|
|
63
76
|
|
|
64
77
|
export function getKnownHostsOptions(persistKnownHosts = true) {
|
|
@@ -2,6 +2,7 @@ import { execa } from 'execa';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { createSpinner, tunnelSpinner } from '../mod/animations.js';
|
|
4
4
|
import { killProcessTree, trackPID, untrackPID } from '../mod/cleanup.js';
|
|
5
|
+
import { getCloudflaredPath } from './platform.js';
|
|
5
6
|
|
|
6
7
|
export async function spawnTunnelSupervised(targetUrl, onUrlGenerated) {
|
|
7
8
|
let isShuttingDown = false;
|
|
@@ -11,8 +12,9 @@ export async function spawnTunnelSupervised(targetUrl, onUrlGenerated) {
|
|
|
11
12
|
while (!isShuttingDown) {
|
|
12
13
|
const spinner = createSpinner('Starting Cloudflare tunnel...', tunnelSpinner).start();
|
|
13
14
|
|
|
15
|
+
const cfExecutable = (await getCloudflaredPath()) || 'cloudflared';
|
|
14
16
|
await new Promise((resolve) => {
|
|
15
|
-
activeChild = execa(
|
|
17
|
+
activeChild = execa(cfExecutable, ['tunnel', '--url', targetUrl], {
|
|
16
18
|
reject: false,
|
|
17
19
|
all: true,
|
|
18
20
|
buffer: false,
|
package/src/modes/client.js
CHANGED
|
@@ -37,6 +37,8 @@ function startLiveLogSync(username, hostname, privateKeyPath, remoteDropPath, lo
|
|
|
37
37
|
let lastSize = -1;
|
|
38
38
|
let lastMtime = 0;
|
|
39
39
|
let isSyncing = false;
|
|
40
|
+
let consecutiveFailures = 0;
|
|
41
|
+
let warnedOnce = false;
|
|
40
42
|
const interval = setInterval(async () => {
|
|
41
43
|
if (isSyncing) return;
|
|
42
44
|
isSyncing = true;
|
|
@@ -65,9 +67,16 @@ function startLiveLogSync(username, hostname, privateKeyPath, remoteDropPath, lo
|
|
|
65
67
|
if (result.exitCode === 0) {
|
|
66
68
|
lastSize = stats.size;
|
|
67
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
|
+
}
|
|
68
77
|
}
|
|
69
78
|
} catch {
|
|
70
|
-
|
|
79
|
+
consecutiveFailures++;
|
|
71
80
|
} finally {
|
|
72
81
|
isSyncing = false;
|
|
73
82
|
}
|
|
@@ -617,6 +626,10 @@ export async function startClientMode(options = {}) {
|
|
|
617
626
|
}
|
|
618
627
|
}
|
|
619
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
|
+
|
|
620
633
|
// Start background E2E client log sync if sharedDropPath is configured
|
|
621
634
|
if (payload.sharedDropPath && sessionLogPath) {
|
|
622
635
|
startLiveLogSync(username, hostname, privateKeyPath, payload.sharedDropPath, sessionLogPath, persistKnownHosts);
|
package/src/modes/doctor.js
CHANGED
|
@@ -7,7 +7,7 @@ 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, isLinuxSSHActive } from '../lib/services/platform.js';
|
|
10
|
+
import { commandExists, detectOS, isLinuxSSHActive, getCloudflaredPath } from '../lib/services/platform.js';
|
|
11
11
|
import { pingBroker } from '../lib/client/broker.js';
|
|
12
12
|
import { classifyCommand, redactSensitive } from '../lib/ai/safety.js';
|
|
13
13
|
|
|
@@ -230,7 +230,8 @@ export async function startDoctorMode(options = {}) {
|
|
|
230
230
|
await check('ssh client', () => commandVersion('ssh', ['-V']));
|
|
231
231
|
await check('scp client', () => commandFound('scp'));
|
|
232
232
|
await check('ssh-keygen', () => commandFound('ssh-keygen'));
|
|
233
|
-
|
|
233
|
+
const cfPath = await getCloudflaredPath();
|
|
234
|
+
await check('cloudflared', () => commandVersion(cfPath || 'cloudflared', ['--version']));
|
|
234
235
|
|
|
235
236
|
console.log(chalk.bold('\n Host readiness'));
|
|
236
237
|
await check('SSH service', checkSshService);
|
package/src/modes/host.js
CHANGED
|
@@ -113,7 +113,7 @@ async function ensureSSHRunning() {
|
|
|
113
113
|
function formatAndPrintLogLine(line) {
|
|
114
114
|
try {
|
|
115
115
|
const data = JSON.parse(line);
|
|
116
|
-
const time = new Date(data.timestamp).toLocaleTimeString();
|
|
116
|
+
const time = new Date(data.time || data.timestamp).toLocaleTimeString();
|
|
117
117
|
const typeLabel = chalk.bold(data.type);
|
|
118
118
|
|
|
119
119
|
let color = chalk.white;
|
|
@@ -317,6 +317,12 @@ async function injectPublicKey(pubKey) {
|
|
|
317
317
|
throw new Error('Could not resolve the current user home directory for authorized_keys');
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
if (process.platform !== 'win32') {
|
|
321
|
+
try {
|
|
322
|
+
await fs.promises.chmod(homedir, 0o755);
|
|
323
|
+
} catch {}
|
|
324
|
+
}
|
|
325
|
+
|
|
320
326
|
const sshDir = path.join(homedir, '.ssh');
|
|
321
327
|
|
|
322
328
|
if (!fs.existsSync(sshDir)) {
|
|
@@ -1140,10 +1146,18 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
1140
1146
|
|
|
1141
1147
|
for (const request of pending) {
|
|
1142
1148
|
let details = {};
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1149
|
+
if (!request.iv || !request.ciphertext || !request.salt) {
|
|
1150
|
+
console.log(chalk.yellow(' ⚠️ Broker did not return encrypted client metadata.'));
|
|
1151
|
+
console.log(chalk.dim(' This usually means the broker is running an older version.'));
|
|
1152
|
+
console.log(chalk.dim(' Redeploy the broker with the latest server.js to fix this.'));
|
|
1153
|
+
details = { error: 'Broker returned no encrypted metadata' };
|
|
1154
|
+
} else {
|
|
1155
|
+
try {
|
|
1156
|
+
details = JSON.parse(await decryptAsync(request.iv, request.ciphertext, password, request.salt));
|
|
1157
|
+
} catch (decErr) {
|
|
1158
|
+
console.log(chalk.yellow(` ⚠️ Could not decrypt client details: ${decErr.message}`));
|
|
1159
|
+
details = { error: 'Decryption failed' };
|
|
1160
|
+
}
|
|
1147
1161
|
}
|
|
1148
1162
|
console.log('');
|
|
1149
1163
|
console.log(chalk.bold.cyan(` Approval Request ${request.id}`));
|
|
@@ -1166,12 +1180,13 @@ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessio
|
|
|
1166
1180
|
if (decision !== 'skip') {
|
|
1167
1181
|
let approvedPayload = null;
|
|
1168
1182
|
if (decision === 'approved') {
|
|
1183
|
+
// Key derivation uses ONLY values both sides reliably know:
|
|
1184
|
+
// password + broker-observed IP. No decrypted client metadata
|
|
1185
|
+
// (which may fail if broker is stale or encryption differs).
|
|
1169
1186
|
const clientKeySalt = [
|
|
1170
1187
|
password,
|
|
1171
1188
|
request.ip || 'unknown',
|
|
1172
|
-
|
|
1173
|
-
details.hostname || 'unknown',
|
|
1174
|
-
details.os || 'unknown'
|
|
1189
|
+
uid
|
|
1175
1190
|
].join('|');
|
|
1176
1191
|
const clientPwd = crypto.createHash('sha256').update(clientKeySalt).digest('hex');
|
|
1177
1192
|
|