@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraj181/ipingyou",
3
- "version": "2.1.22",
3
+ "version": "2.1.23",
4
4
  "description": "SecureLink-CLI — Secure peer-to-peer remote access via SSH & Cloudflare Tunnels",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
@@ -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
- os.userInfo().username,
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('Decryption failed — incorrect password or corrupted data');
231
- if (!spinner) console.error(chalk.red(' ❌ Error: Could not decrypt tunnel data. Incorrect password.'));
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 hasBrew = await commandExists('brew');
230
- if (hasBrew) {
231
- await executeWithRetry('brew', ['install', 'cloudflared']);
232
- } else {
233
- console.log(chalk.yellow(' Homebrew not found. Downloading cloudflared binary directly...'));
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 hasWinget = await commandExists('winget');
244
- if (hasWinget) {
245
- await executeWithRetry('winget', ['install', '--id', 'Cloudflare.cloudflared', '--silent', '--accept-source-agreements', '--accept-package-agreements']);
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
- let installed = false;
261
- if (distro === 'debian' || (await commandExists('apt-get'))) {
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: await commandExists('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: await commandExists('cloudflared'),
299
+ cloudflared: finalCfPath !== null,
344
300
  };
345
301
 
346
302
  const stillMissing = Object.entries(results)
@@ -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
- return ['-o', `ProxyCommand=cloudflared access tcp --hostname ${safeHostname}`];
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('cloudflared', ['tunnel', '--url', targetUrl], {
17
+ activeChild = execa(cfExecutable, ['tunnel', '--url', targetUrl], {
16
18
  reject: false,
17
19
  all: true,
18
20
  buffer: false,
@@ -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
- // Ignore background sync failures silently
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);
@@ -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
- await check('cloudflared', () => commandVersion('cloudflared', ['--version']));
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
- try {
1144
- details = JSON.parse(await decryptAsync(request.iv, request.ciphertext, password, request.salt));
1145
- } catch {
1146
- details = { error: 'Could not decrypt request metadata' };
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
- details.username || 'unknown',
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