@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 +2 -2
- package/src/lib/broker.js +4 -1
- package/src/modes/host.js +26 -25
- package/src/server.js +6 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miraj181/ipingyou",
|
|
3
|
-
"version": "2.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": "^
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
1013
|
-
await waitForValue(() =>
|
|
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,
|
|
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
|
-
//
|
|
223
|
-
const hostToken =
|
|
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, {
|