@miraj181/ipingyou 2.1.1 → 2.1.3

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.1",
3
+ "version": "2.1.3",
4
4
  "description": "SecureLink-CLI — Secure peer-to-peer remote access via SSH & Cloudflare Tunnels",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
@@ -55,7 +55,7 @@
55
55
  "ws": "^8.20.1",
56
56
  "express": "^5.2.1",
57
57
  "express-rate-limit": "^8.5.2",
58
- "helmet": "^7.1.0"
58
+ "helmet": "^8.2.0"
59
59
  },
60
60
  "devDependencies": {
61
61
  "nodemon": "^3.1.4"
package/src/lib/broker.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import os from 'node:os';
3
+ import crypto from 'node:crypto';
3
4
  import { decrypt, encrypt } from './crypto.js';
4
5
  import { createSpinner, cryptoSpinner, networkSpinner } from './animations.js';
5
6
  import { logSessionEvent } from './session-log.js';
@@ -51,6 +52,7 @@ export async function registerWithBroker(brokerUrl, uid, tunnelUrl, password, se
51
52
  await new Promise(r => setTimeout(r, 600));
52
53
  const payload = JSON.stringify({ url: tunnelUrl, ...serviceConfig });
53
54
  const encrypted = encrypt(payload, password);
55
+ const localHostToken = crypto.randomBytes(32).toString('hex');
54
56
 
55
57
  spinner.text = 'Registering with broker...';
56
58
 
@@ -64,6 +66,7 @@ export async function registerWithBroker(brokerUrl, uid, tunnelUrl, password, se
64
66
  salt: encrypted.salt,
65
67
  approvalRequired: Boolean(serviceConfig.approvalRequired),
66
68
  oneTime: Boolean(serviceConfig.oneTimeSharePath),
69
+ hostToken: localHostToken,
67
70
  }),
68
71
  });
69
72
 
@@ -77,7 +80,7 @@ export async function registerWithBroker(brokerUrl, uid, tunnelUrl, password, se
77
80
  spinner.succeed(`Registered with broker ${chalk.dim(`(${brokerUrl})`)} ${chalk.green('[E2E encrypted]')}`);
78
81
  logSessionEvent('broker_registered', { uid, broker: brokerUrl });
79
82
  // Return the host authentication token — needed for all host-only broker operations
80
- return { success: true, hostToken: result.hostToken || null };
83
+ return { success: true, hostToken: result.hostToken || localHostToken || null };
81
84
  } catch (err) {
82
85
  spinner.fail(`Broker registration failed: ${err.message}`);
83
86
  console.error(chalk.red(` ❌ Error: ${err.message}`));
package/src/modes/host.js CHANGED
@@ -221,7 +221,7 @@ async function promptOneTimeSharePath() {
221
221
  return sharePath.trim() === '~' ? os.homedir() : sharePath.trim().replace(/^~(?=\/)/, os.homedir());
222
222
  }
223
223
 
224
- async function startLocalHostDashboard(uid, password, serviceConfig, hostToken) {
224
+ async function startLocalHostDashboard(uid, password, serviceConfig, sessionState) {
225
225
  const { default: express } = await import('express');
226
226
  const { default: open } = await import('open');
227
227
  const app = express();
@@ -251,7 +251,7 @@ async function startLocalHostDashboard(uid, password, serviceConfig, hostToken)
251
251
  const push = async () => {
252
252
  if (closed) return;
253
253
  try {
254
- const data = await fetchApprovalRequests(BROKER_URL, uid, hostToken).catch(() => ({ approvals: [] }));
254
+ const data = await fetchApprovalRequests(BROKER_URL, uid, sessionState.hostToken).catch(() => ({ approvals: [] }));
255
255
  res.write(`event: approvals\n`);
256
256
  res.write(`data: ${JSON.stringify(data)}\n\n`);
257
257
  } catch { }
@@ -286,7 +286,7 @@ async function startLocalHostDashboard(uid, password, serviceConfig, hostToken)
286
286
  const { requestId, decision } = req.body || {};
287
287
  if (!requestId || !decision) return res.status(400).json({ error: 'requestId and decision required' });
288
288
  try {
289
- await decideApprovalRequest(BROKER_URL, uid, requestId, decision, hostToken);
289
+ await decideApprovalRequest(BROKER_URL, uid, requestId, decision, sessionState.hostToken);
290
290
  recordEvent('approval_decision', { uid, requestId, decision, via: 'dashboard' });
291
291
  res.json({ ok: true });
292
292
  } catch (err) {
@@ -296,7 +296,7 @@ async function startLocalHostDashboard(uid, password, serviceConfig, hostToken)
296
296
 
297
297
  app.post('/api/revoke', async (_req, res) => {
298
298
  try {
299
- await revokeUID(BROKER_URL, uid, hostToken);
299
+ await revokeUID(BROKER_URL, uid, sessionState.hostToken);
300
300
  recordEvent('uid_revoked', { uid, via: 'dashboard' });
301
301
  res.json({ ok: true });
302
302
  } catch (err) {
@@ -545,7 +545,7 @@ async function spawnPrivateBroker() {
545
545
  /**
546
546
  * Display the host dashboard and handle user input.
547
547
  */
548
- async function hostDashboard(uid, tunnelUrl, password, serviceConfig, tunnelProcess, hostToken) {
548
+ async function hostDashboard(uid, password, serviceConfig, tunnelProcess, sessionState) {
549
549
  let chatServerInstance = null;
550
550
  let chatTunnelProcess = null;
551
551
  let dashboardInstance = null;
@@ -559,7 +559,7 @@ async function hostDashboard(uid, tunnelUrl, password, serviceConfig, tunnelProc
559
559
  console.log(` ║ ${chalk.cyan('UID:')} ${chalk.bold.white(uid.padEnd(30))}║`);
560
560
  console.log(` ║ ${chalk.cyan('Password:')} ${chalk.bold.white(password.padEnd(30))}║`);
561
561
  console.log(` ║ ${chalk.cyan('Service:')} ${chalk.dim(serviceConfig.type.toUpperCase() + ' (Port ' + serviceConfig.port + ')').padEnd(30)}║`);
562
- console.log(` ║ ${chalk.cyan('Tunnel:')} ${chalk.dim(tunnelUrl.substring(0, 40))} ║`);
562
+ console.log(` ║ ${chalk.cyan('Tunnel:')} ${chalk.dim(sessionState.tunnelUrl.substring(0, 40))} ║`);
563
563
  if (serviceConfig.chatUrl) {
564
564
  console.log(` ║ ${chalk.cyan('Chat URL:')} ${chalk.dim(serviceConfig.chatUrl.substring(0, 40))} ║`);
565
565
  }
@@ -624,7 +624,7 @@ async function hostDashboard(uid, tunnelUrl, password, serviceConfig, tunnelProc
624
624
  switch (action) {
625
625
  case 'approvals': {
626
626
  try {
627
- const data = await fetchApprovalRequests(BROKER_URL, uid);
627
+ const data = await fetchApprovalRequests(BROKER_URL, uid, sessionState.hostToken);
628
628
  const pending = (data.approvals || []).filter(item => item.status === 'pending');
629
629
  if (pending.length === 0) {
630
630
  console.log(chalk.yellow(' No pending approval requests.'));
@@ -656,7 +656,7 @@ async function hostDashboard(uid, tunnelUrl, password, serviceConfig, tunnelProc
656
656
  ],
657
657
  }]);
658
658
  if (decision !== 'skip') {
659
- await decideApprovalRequest(BROKER_URL, uid, request.id, decision);
659
+ await decideApprovalRequest(BROKER_URL, uid, request.id, decision, sessionState.hostToken);
660
660
  recordEvent('approval_decision', { uid, requestId: request.id, decision, username: details.username });
661
661
  }
662
662
  }
@@ -675,8 +675,8 @@ async function hostDashboard(uid, tunnelUrl, password, serviceConfig, tunnelProc
675
675
  chatTunnelProcess = null;
676
676
  chatServerInstance = null;
677
677
  delete serviceConfig.chatUrl;
678
- const res = await registerWithBroker(BROKER_URL, uid, tunnelUrl, password, serviceConfig);
679
- if (res.success && res.hostToken) hostToken = res.hostToken;
678
+ const res = await registerWithBroker(BROKER_URL, uid, sessionState.tunnelUrl, password, serviceConfig);
679
+ if (res.success && res.hostToken) sessionState.hostToken = res.hostToken;
680
680
  renderDashboard();
681
681
  }
682
682
  });
@@ -684,8 +684,8 @@ async function hostDashboard(uid, tunnelUrl, password, serviceConfig, tunnelProc
684
684
  console.log(chalk.dim(' Provisioning Cloudflare tunnel for chat...'));
685
685
  chatTunnelProcess = await spawnTunnelSupervised(`http://localhost:${chatServerInstance.port}`, async (newUrl) => {
686
686
  serviceConfig.chatUrl = newUrl;
687
- const res = await registerWithBroker(BROKER_URL, uid, tunnelUrl, password, serviceConfig);
688
- if (res.success && res.hostToken) hostToken = res.hostToken;
687
+ const res = await registerWithBroker(BROKER_URL, uid, sessionState.tunnelUrl, password, serviceConfig);
688
+ if (res.success && res.hostToken) sessionState.hostToken = res.hostToken;
689
689
  renderDashboard();
690
690
  });
691
691
 
@@ -698,7 +698,7 @@ async function hostDashboard(uid, tunnelUrl, password, serviceConfig, tunnelProc
698
698
  }
699
699
 
700
700
  case 'dashboard': {
701
- dashboardInstance = await startLocalHostDashboard(uid, password, serviceConfig, hostToken);
701
+ dashboardInstance = await startLocalHostDashboard(uid, password, serviceConfig, sessionState);
702
702
  logSessionEvent('host_dashboard_opened');
703
703
  return waitForAction();
704
704
  }
@@ -747,7 +747,9 @@ async function hostDashboard(uid, tunnelUrl, password, serviceConfig, tunnelProc
747
747
  case 'show': {
748
748
  const spinner = createSpinner('Fetching secure client telemetry...', networkSpinner).start();
749
749
  try {
750
- const res = await fetch(`${BROKER_URL}/clients/${uid}`);
750
+ const res = await fetch(`${BROKER_URL}/clients/${uid}`, {
751
+ headers: sessionState.hostToken ? { 'x-host-token': sessionState.hostToken } : {}
752
+ });
751
753
  if (!res.ok) throw new Error('Failed to fetch from broker');
752
754
  const data = await res.json();
753
755
 
@@ -784,8 +786,8 @@ async function hostDashboard(uid, tunnelUrl, password, serviceConfig, tunnelProc
784
786
  }
785
787
 
786
788
  case 'reregister':
787
- const res = await registerWithBroker(BROKER_URL, uid, tunnelUrl, password, serviceConfig);
788
- if (res.success && res.hostToken) hostToken = res.hostToken;
789
+ const res = await registerWithBroker(BROKER_URL, uid, sessionState.tunnelUrl, password, serviceConfig);
790
+ if (res.success && res.hostToken) sessionState.hostToken = res.hostToken;
789
791
  logSessionEvent('host_broker_reregistered');
790
792
  return waitForAction();
791
793
 
@@ -995,24 +997,23 @@ export async function startHostMode() {
995
997
  console.log(chalk.dim(` ℹ️ Ensure your ${protocol.toUpperCase()} service is running on port ${targetPort}.`));
996
998
  }
997
999
 
998
- let tunnelUrl = null;
999
- let hostToken = null;
1000
+ const sessionState = { tunnelUrl: null, hostToken: null };
1000
1001
  const tunnelProcess = await spawnTunnelSupervised(targetUrl, async (newUrl) => {
1001
- tunnelUrl = newUrl;
1002
+ sessionState.tunnelUrl = newUrl;
1002
1003
  // Register or re-register with broker when tunnel is spawned/respawned
1003
- const res = await registerWithBroker(BROKER_URL, uid, tunnelUrl, password, serviceConfig);
1004
+ const res = await registerWithBroker(BROKER_URL, uid, sessionState.tunnelUrl, password, serviceConfig);
1004
1005
  if (!res.success) {
1005
1006
  console.error(chalk.red(`\n ❌ FATAL: Could not register with broker at ${BROKER_URL}`));
1006
1007
  logSessionEvent('host_broker_register_failed', { broker: BROKER_URL }, 'error');
1007
1008
  process.exit(1);
1008
1009
  }
1009
- if (res.hostToken) hostToken = res.hostToken;
1010
+ if (res.hostToken) sessionState.hostToken = res.hostToken;
1010
1011
  });
1011
1012
 
1012
- // Wait for the first URL to be generated before showing the dashboard
1013
- await waitForValue(() => tunnelUrl, 30000, 'Cloudflare tunnel startup');
1013
+ // Wait for the tunnel AND broker registration to complete before showing dashboard
1014
+ await waitForValue(() => sessionState.hostToken, 30000, 'Cloudflare tunnel and Broker startup');
1014
1015
 
1015
- setRevokeOnExit(uid, BROKER_URL, () => hostToken);
1016
+ setRevokeOnExit(uid, BROKER_URL, () => sessionState.hostToken);
1016
1017
 
1017
- await hostDashboard(uid, tunnelUrl, password, serviceConfig, tunnelProcess, hostToken);
1018
+ await hostDashboard(uid, password, serviceConfig, tunnelProcess, sessionState);
1018
1019
  }
package/src/server.js CHANGED
@@ -124,6 +124,7 @@ const store = new Map(); // uid → { iv, ciphertext, salt, createdAt, clients:
124
124
 
125
125
  // ─── Security Helpers ────────────────────────────────────────
126
126
  const SAFE_PARAM = /^[a-zA-Z0-9_-]{1,64}$/;
127
+ const HOST_TOKEN_FORMAT = /^[a-f0-9]{64}$/i;
127
128
 
128
129
  function isSafeParam(val) {
129
130
  return typeof val === 'string' && SAFE_PARAM.test(val);
@@ -185,7 +186,7 @@ app.get('/health', (_req, res) => {
185
186
  */
186
187
  app.post('/register', strictLimiter, (req, res) => {
187
188
  try {
188
- const { uid, iv, ciphertext, salt, approvalRequired = false, oneTime = false } = req.body;
189
+ const { uid, iv, ciphertext, salt, approvalRequired = false, oneTime = false, hostToken: providedHostToken } = req.body;
189
190
 
190
191
  if (!uid || !iv || !ciphertext || !salt) {
191
192
  recordViolation(req);
@@ -219,8 +220,10 @@ app.post('/register', strictLimiter, (req, res) => {
219
220
  return res.status(503).json({ error: 'Broker is at maximum capacity. Please try again later.' });
220
221
  }
221
222
 
222
- // Generate a cryptographic host token only the host receives this
223
- const hostToken = generateHostToken(uid + Date.now().toString());
223
+ // Use provided host token if valid; otherwise generate a fresh one
224
+ const hostToken = (typeof providedHostToken === 'string' && HOST_TOKEN_FORMAT.test(providedHostToken))
225
+ ? providedHostToken
226
+ : generateHostToken(uid + Date.now().toString());
224
227
 
225
228
  // Store the encrypted blob as-is — broker NEVER decrypts
226
229
  store.set(uid, {