@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraj181/ipingyou",
3
- "version": "2.0.5",
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/cli.js",
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
- // For updating participant count dynamically if we wanted to extract sender,
295
- // but since E2E hides sender, we rely on connection count
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 });
@@ -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;