@miraj181/ipingyou 2.0.5 → 2.0.8
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 +8 -4
- package/src/lib/chat.js +3 -9
- package/src/modes/client.js +3 -3
- package/src/modes/host.js +0 -2
- package/src/server.js +263 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miraj181/ipingyou",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.8",
|
|
4
4
|
"description": "SecureLink-CLI — Secure peer-to-peer remote access via SSH & Cloudflare Tunnels",
|
|
5
5
|
"main": "src/cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"src/cli.js",
|
|
12
|
+
"src/server.js",
|
|
12
13
|
"src/lib/",
|
|
13
14
|
"src/modes/",
|
|
14
15
|
"README.md",
|
|
15
16
|
"LICENSE"
|
|
16
17
|
],
|
|
17
18
|
"scripts": {
|
|
18
|
-
"start": "node src/
|
|
19
|
+
"start": "node src/server.js",
|
|
19
20
|
"dev:cli": "node src/cli.js",
|
|
20
21
|
"prepublishOnly": "node -e \"require('fs').accessSync('LICENSE')\" && node src/cli.js --help"
|
|
21
22
|
},
|
|
@@ -50,7 +51,10 @@
|
|
|
50
51
|
"open": "^11.0.0",
|
|
51
52
|
"ora": "^8.1.1",
|
|
52
53
|
"tree-kill": "^1.2.2",
|
|
53
|
-
"ws": "^8.20.1"
|
|
54
|
+
"ws": "^8.20.1",
|
|
55
|
+
"express": "^4.19.2",
|
|
56
|
+
"express-rate-limit": "^7.3.1",
|
|
57
|
+
"helmet": "^7.1.0"
|
|
54
58
|
},
|
|
55
59
|
"devDependencies": {
|
|
56
60
|
"nodemon": "^3.1.4"
|
|
@@ -59,4 +63,4 @@
|
|
|
59
63
|
"node": ">=18.0.0"
|
|
60
64
|
},
|
|
61
65
|
"type": "module"
|
|
62
|
-
}
|
|
66
|
+
}
|
package/src/lib/chat.js
CHANGED
|
@@ -190,6 +190,7 @@ const HTML_CONTENT = `
|
|
|
190
190
|
input.focus();
|
|
191
191
|
|
|
192
192
|
const encPayload = await encryptPayload({ type: 'join', sender: username });
|
|
193
|
+
ws.send(JSON.stringify({ type: 'join_event', username })); // Send unencrypted name for sidebar
|
|
193
194
|
ws.send(JSON.stringify({ type: 'e2e', payload: encPayload }));
|
|
194
195
|
};
|
|
195
196
|
|
|
@@ -291,8 +292,8 @@ export async function startChatServer(onClose) {
|
|
|
291
292
|
// E2E messages just get forwarded to all clients
|
|
292
293
|
broadcastMsg(data);
|
|
293
294
|
} else if (data.type === 'join_event') {
|
|
294
|
-
|
|
295
|
-
|
|
295
|
+
clients.set(ws, data.username || `User_${Math.floor(Math.random()*1000)}`);
|
|
296
|
+
broadcastState();
|
|
296
297
|
} else if (data.type === 'host_close') {
|
|
297
298
|
broadcastMsg({ type: 'close' });
|
|
298
299
|
server.close();
|
|
@@ -304,18 +305,11 @@ export async function startChatServer(onClose) {
|
|
|
304
305
|
});
|
|
305
306
|
|
|
306
307
|
ws.on('close', () => {
|
|
307
|
-
// Since we can't read the encrypted username, we just update state
|
|
308
308
|
clients.delete(ws);
|
|
309
309
|
broadcastState();
|
|
310
310
|
});
|
|
311
311
|
});
|
|
312
312
|
|
|
313
|
-
// When connection opens, just assign a generic ID to count them
|
|
314
|
-
wss.on('connection', (ws) => {
|
|
315
|
-
clients.set(ws, `User_${Math.floor(Math.random()*1000)}`);
|
|
316
|
-
broadcastState();
|
|
317
|
-
});
|
|
318
|
-
|
|
319
313
|
server.listen(0, '127.0.0.1', () => {
|
|
320
314
|
const port = server.address().port;
|
|
321
315
|
resolve({ port, server });
|
package/src/modes/client.js
CHANGED
|
@@ -124,14 +124,14 @@ async function resolveUID(uid, password, silent = false) {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
if (!payloadConfig.url.startsWith('https://')) {
|
|
127
|
-
spinner.fail('Decrypted data is not a valid tunnel URL (incorrect password)');
|
|
127
|
+
if (spinner) spinner.fail('Decrypted data is not a valid tunnel URL (incorrect password)');
|
|
128
128
|
return null;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
spinner.succeed(`Resolved: ${chalk.dim(payloadConfig.url)} ${chalk.green('[decrypted locally]')}`);
|
|
131
|
+
if (spinner) spinner.succeed(`Resolved: ${chalk.dim(payloadConfig.url)} ${chalk.green('[decrypted locally]')}`);
|
|
132
132
|
return payloadConfig;
|
|
133
133
|
} catch (err) {
|
|
134
|
-
spinner.fail(`Broker lookup failed: ${err.message}`);
|
|
134
|
+
if (spinner) spinner.fail(`Broker lookup failed: ${err.message}`);
|
|
135
135
|
return null;
|
|
136
136
|
}
|
|
137
137
|
}
|
package/src/modes/host.js
CHANGED
|
@@ -449,14 +449,12 @@ async function hostDashboard(uid, tunnelUrl, password, serviceConfig, tunnelProc
|
|
|
449
449
|
}
|
|
450
450
|
|
|
451
451
|
case 'exit':
|
|
452
|
-
clearInterval(monitorInterval);
|
|
453
452
|
if (tunnelProcess) tunnelProcess.kill();
|
|
454
453
|
if (chatTunnelProcess) chatTunnelProcess.kill();
|
|
455
454
|
if (global.privateBrokerInstance) global.privateBrokerInstance.kill();
|
|
456
455
|
return;
|
|
457
456
|
}
|
|
458
457
|
} catch (err) {
|
|
459
|
-
clearInterval(monitorInterval);
|
|
460
458
|
throw err;
|
|
461
459
|
}
|
|
462
460
|
};
|
package/src/server.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================
|
|
3
|
+
* SecureLink-CLI — Central Broker Server
|
|
4
|
+
* ============================================================
|
|
5
|
+
* A lightweight REST relay that pairs hosts and clients via
|
|
6
|
+
* temporary UIDs. The broker is a DUMB PIPE — it stores and
|
|
7
|
+
* returns encrypted blobs without ever seeing plaintext.
|
|
8
|
+
* Encryption/decryption happens ONLY on the CLI side.
|
|
9
|
+
* ============================================================
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import express from 'express';
|
|
13
|
+
import rateLimit from 'express-rate-limit';
|
|
14
|
+
import helmet from 'helmet';
|
|
15
|
+
|
|
16
|
+
const app = express();
|
|
17
|
+
|
|
18
|
+
// ─── Process-Level Error Handling ────────────────────────────
|
|
19
|
+
process.on('uncaughtException', (err) => {
|
|
20
|
+
console.error('💥 Uncaught Exception:', err.message);
|
|
21
|
+
if (err.stack) console.error(err.stack);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
26
|
+
console.error('💥 Unhandled Rejection at:', promise, 'reason:', reason);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// ─── Basic Security Headers ──────────────────────────────────
|
|
30
|
+
app.use(helmet());
|
|
31
|
+
app.use(express.json({ limit: '10kb' })); // Limit JSON payload size to prevent payload-based DoS
|
|
32
|
+
|
|
33
|
+
// ─── Rate Limiters ───────────────────────────────────────────
|
|
34
|
+
// Trust proxy is required if the server is behind a reverse proxy (like Render, Heroku, Cloudflare)
|
|
35
|
+
app.set('trust proxy', 1);
|
|
36
|
+
|
|
37
|
+
// General rate limiter for all requests (100 reqs per 15 minutes per IP)
|
|
38
|
+
const generalLimiter = rateLimit({
|
|
39
|
+
windowMs: 15 * 60 * 1000,
|
|
40
|
+
max: 100,
|
|
41
|
+
message: { error: 'Too many requests from this IP, please try again after 15 minutes.' },
|
|
42
|
+
standardHeaders: true,
|
|
43
|
+
legacyHeaders: false,
|
|
44
|
+
});
|
|
45
|
+
app.use(generalLimiter);
|
|
46
|
+
|
|
47
|
+
// Stricter rate limiter for registration/revocation endpoints (e.g. 20 reqs per 15 mins)
|
|
48
|
+
const strictLimiter = rateLimit({
|
|
49
|
+
windowMs: 15 * 60 * 1000,
|
|
50
|
+
max: 20,
|
|
51
|
+
message: { error: 'Too many registration/revocation requests. Please try again later.' },
|
|
52
|
+
standardHeaders: true,
|
|
53
|
+
legacyHeaders: false,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// ─── Active Defense & IP Blacklisting (IDS) ──────────────────
|
|
57
|
+
const ipViolations = new Map(); // ip → violation_count
|
|
58
|
+
const blacklistedIPs = new Set();
|
|
59
|
+
const VIOLATION_THRESHOLD = 5; // Block IP after 5 malicious requests
|
|
60
|
+
|
|
61
|
+
app.use((req, res, next) => {
|
|
62
|
+
const ip = req.ip || req.connection.remoteAddress;
|
|
63
|
+
if (blacklistedIPs.has(ip)) {
|
|
64
|
+
console.warn(`🛡️ Dropped traffic from blacklisted IP: ${ip}`);
|
|
65
|
+
return res.status(403).json({ error: 'Your IP has been banned due to suspicious activity.' });
|
|
66
|
+
}
|
|
67
|
+
next();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
function recordViolation(req) {
|
|
71
|
+
const ip = req.ip || req.connection.remoteAddress;
|
|
72
|
+
const count = (ipViolations.get(ip) || 0) + 1;
|
|
73
|
+
ipViolations.set(ip, count);
|
|
74
|
+
console.warn(`🚨 Violation recorded for IP ${ip} (${count}/${VIOLATION_THRESHOLD})`);
|
|
75
|
+
|
|
76
|
+
if (count >= VIOLATION_THRESHOLD) {
|
|
77
|
+
blacklistedIPs.add(ip);
|
|
78
|
+
console.error(`💥 HACKING DETECTED: Auto-banned IP ${ip} to defend server.`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── In-memory store — auto-expire after TTL ─────────────────
|
|
83
|
+
const TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
84
|
+
const store = new Map(); // uid → { iv, ciphertext, salt, createdAt, clients: [] }
|
|
85
|
+
|
|
86
|
+
function pruneExpired() {
|
|
87
|
+
const now = Date.now();
|
|
88
|
+
for (const [uid, entry] of store) {
|
|
89
|
+
if (now - entry.createdAt > TTL_MS) {
|
|
90
|
+
store.delete(uid);
|
|
91
|
+
console.log(`🗑️ Expired UID: ${uid}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Run pruning every 5 minutes
|
|
97
|
+
setInterval(pruneExpired, 5 * 60 * 1000);
|
|
98
|
+
|
|
99
|
+
// ─── Routes ──────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
// Health
|
|
102
|
+
app.get('/health', (_req, res) => {
|
|
103
|
+
res.json({ status: 'ok', uptime: process.uptime(), activeUIDs: store.size });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* POST /register
|
|
108
|
+
* Body: { uid: string, iv: string, ciphertext: string, salt: string }
|
|
109
|
+
*
|
|
110
|
+
* The broker receives an ALREADY-ENCRYPTED blob from the host CLI.
|
|
111
|
+
* It never decrypts — just stores the { iv, ciphertext } pair.
|
|
112
|
+
*/
|
|
113
|
+
app.post('/register', strictLimiter, (req, res) => {
|
|
114
|
+
try {
|
|
115
|
+
const { uid, iv, ciphertext, salt } = req.body;
|
|
116
|
+
|
|
117
|
+
if (!uid || !iv || !ciphertext || !salt) {
|
|
118
|
+
recordViolation(req);
|
|
119
|
+
return res.status(400).json({ error: 'Missing uid, iv, ciphertext, or salt' });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (typeof uid !== 'string' || uid.length < 6 || uid.length > 16) {
|
|
123
|
+
recordViolation(req);
|
|
124
|
+
return res.status(400).json({ error: 'Invalid UID format (6-16 chars)' });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Validate IV format — must be 32 hex chars (16 bytes)
|
|
128
|
+
if (!/^[a-f0-9]{32}$/i.test(iv)) {
|
|
129
|
+
recordViolation(req);
|
|
130
|
+
return res.status(400).json({ error: 'Invalid IV format (expected 32 hex chars)' });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Validate ciphertext is non-empty base64
|
|
134
|
+
if (typeof ciphertext !== 'string' || ciphertext.length === 0) {
|
|
135
|
+
recordViolation(req);
|
|
136
|
+
return res.status(400).json({ error: 'Invalid ciphertext' });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Store the encrypted blob as-is — broker NEVER decrypts
|
|
140
|
+
store.set(uid, {
|
|
141
|
+
iv,
|
|
142
|
+
ciphertext,
|
|
143
|
+
salt,
|
|
144
|
+
createdAt: Date.now(),
|
|
145
|
+
clients: []
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
console.log(`✅ [${new Date().toLocaleTimeString()}] Registered UID: ${uid} (encrypted, ${ciphertext.length} bytes)`);
|
|
149
|
+
res.json({ status: 'registered', uid, expiresIn: '1 hour' });
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.error('❌ Register error:', err.message);
|
|
152
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* GET /resolve/:uid
|
|
158
|
+
* Returns the ENCRYPTED blob { iv, ciphertext } for a given UID.
|
|
159
|
+
* The client CLI decrypts it locally — the broker never sees plaintext.
|
|
160
|
+
*/
|
|
161
|
+
app.get('/resolve/:uid', (req, res) => {
|
|
162
|
+
try {
|
|
163
|
+
const { uid } = req.params;
|
|
164
|
+
const entry = store.get(uid);
|
|
165
|
+
|
|
166
|
+
if (!entry) {
|
|
167
|
+
// Don't strictly record a violation for one typo, but repeated 404s will be caught by rate limiter
|
|
168
|
+
return res.status(404).json({ error: 'UID not found or expired' });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Check TTL
|
|
172
|
+
if (Date.now() - entry.createdAt > TTL_MS) {
|
|
173
|
+
store.delete(uid);
|
|
174
|
+
return res.status(410).json({ error: 'UID expired' });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
console.log(`🔍 [${new Date().toLocaleTimeString()}] Resolved UID: ${uid} (returning encrypted blob)`);
|
|
178
|
+
|
|
179
|
+
// Return encrypted blob — client decrypts
|
|
180
|
+
res.json({ uid, iv: entry.iv, ciphertext: entry.ciphertext, salt: entry.salt });
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.error('❌ Resolve error:', err.message);
|
|
183
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* POST /client-info/:uid
|
|
189
|
+
* Clients securely post their encrypted hardware telemetry.
|
|
190
|
+
*/
|
|
191
|
+
app.post('/client-info/:uid', generalLimiter, (req, res) => {
|
|
192
|
+
try {
|
|
193
|
+
const { uid } = req.params;
|
|
194
|
+
const { iv, ciphertext, salt } = req.body;
|
|
195
|
+
|
|
196
|
+
const entry = store.get(uid);
|
|
197
|
+
if (!entry) return res.status(404).json({ error: 'UID not found' });
|
|
198
|
+
if (!iv || !ciphertext || !salt) return res.status(400).json({ error: 'Missing encrypted telemetry payload' });
|
|
199
|
+
|
|
200
|
+
entry.clients.push({ iv, ciphertext, salt, seenAt: Date.now() });
|
|
201
|
+
|
|
202
|
+
// Keep max 50 recent client pings to prevent memory leaks
|
|
203
|
+
if (entry.clients.length > 50) entry.clients.shift();
|
|
204
|
+
|
|
205
|
+
res.json({ status: 'ok' });
|
|
206
|
+
} catch (err) {
|
|
207
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* GET /clients/:uid
|
|
213
|
+
* Host retrieves all securely encrypted client telemetry blobs.
|
|
214
|
+
*/
|
|
215
|
+
app.get('/clients/:uid', generalLimiter, (req, res) => {
|
|
216
|
+
try {
|
|
217
|
+
const { uid } = req.params;
|
|
218
|
+
const entry = store.get(uid);
|
|
219
|
+
|
|
220
|
+
if (!entry) return res.status(404).json({ error: 'UID not found' });
|
|
221
|
+
|
|
222
|
+
res.json({ clients: entry.clients });
|
|
223
|
+
} catch (err) {
|
|
224
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* DELETE /revoke/:uid
|
|
230
|
+
* Allows host to explicitly remove their UID before expiry.
|
|
231
|
+
*/
|
|
232
|
+
app.delete('/revoke/:uid', strictLimiter, (req, res) => {
|
|
233
|
+
const { uid } = req.params;
|
|
234
|
+
const existed = store.delete(uid);
|
|
235
|
+
console.log(`🚫 [${new Date().toLocaleTimeString()}] Revoked UID: ${uid} (existed: ${existed})`);
|
|
236
|
+
res.json({ status: existed ? 'revoked' : 'not_found' });
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// ─── Global Error Handler ────────────────────────────────────
|
|
240
|
+
app.use((err, req, res, next) => {
|
|
241
|
+
console.error('❌ Express Error:', err.message);
|
|
242
|
+
if (err.stack) console.error(err.stack);
|
|
243
|
+
res.status(err.status || 500).json({ error: 'Internal Server Error' });
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// ─── Launch ──────────────────────────────────────────────────
|
|
247
|
+
// Render injects PORT; locally we use BROKER_PORT; fallback 4000
|
|
248
|
+
const PORT = process.env.PORT || process.env.BROKER_PORT || 4000;
|
|
249
|
+
const HOST = process.env.HOST || '0.0.0.0';
|
|
250
|
+
|
|
251
|
+
app.listen(PORT, HOST, () => {
|
|
252
|
+
console.log('');
|
|
253
|
+
console.log(' ╔══════════════════════════════════════════╗');
|
|
254
|
+
console.log(' ║ 🔗 SecureLink Broker — Active ║');
|
|
255
|
+
console.log(` ║ 📡 Port: ${String(PORT).padEnd(29)}║`);
|
|
256
|
+
console.log(' ║ 🔒 Zero-Knowledge Encrypted Store ║');
|
|
257
|
+
console.log(' ║ ⏱️ TTL: 1 hour per UID ║');
|
|
258
|
+
console.log(' ║ 🚫 Broker NEVER sees plaintext ║');
|
|
259
|
+
console.log(' ╚══════════════════════════════════════════╝');
|
|
260
|
+
console.log('');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
export default app;
|