@miraj181/ipingyou 2.1.9 → 2.1.18

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/src/server.js CHANGED
@@ -96,8 +96,8 @@ const hostLimiter = rateLimit({
96
96
  });
97
97
 
98
98
  // ─── Active Defense & IP Blacklisting (IDS) ──────────────────
99
- const ipViolations = new Map(); // ip → violation_count
100
- const blacklistedIPs = new Set();
99
+ const ipViolations = new Map(); // ip → { count, lastSeen }
100
+ const blacklistedIPs = new Map(); // ip → bannedAt
101
101
  const VIOLATION_THRESHOLD = 5; // Block IP after 5 malicious requests
102
102
 
103
103
  app.use((req, res, next) => {
@@ -111,12 +111,12 @@ app.use((req, res, next) => {
111
111
 
112
112
  function recordViolation(req) {
113
113
  const ip = req.ip || req.connection.remoteAddress;
114
- const count = (ipViolations.get(ip) || 0) + 1;
115
- ipViolations.set(ip, count);
114
+ const count = (ipViolations.get(ip)?.count || 0) + 1;
115
+ ipViolations.set(ip, { count, lastSeen: Date.now() });
116
116
  console.warn(`🚨 Violation recorded for IP ${ip} (${count}/${VIOLATION_THRESHOLD})`);
117
117
 
118
118
  if (count >= VIOLATION_THRESHOLD) {
119
- blacklistedIPs.add(ip);
119
+ blacklistedIPs.set(ip, Date.now());
120
120
  console.error(`💥 HACKING DETECTED: Auto-banned IP ${ip} to defend server.`);
121
121
  }
122
122
  }
@@ -126,9 +126,19 @@ const TTL_MS = 60 * 60 * 1000; // 1 hour
126
126
  const MAX_UIDS = 50000; // Max concurrent tunnels (prevent memory leak)
127
127
  const MAX_VIOLATIONS = 50000; // Max tracked malicious IPs before reset
128
128
  const MAX_RESOLVES_PER_UID = 100; // Max resolves before auto-revoke (anti-scraping)
129
+ const MAX_APPROVALS_PER_UID = 50;
130
+ const MAX_CLIENTS_PER_UID = 50;
131
+ const configuredPayloadMb = Number.parseInt(process.env.BROKER_MAX_PAYLOAD_MB || '64', 10);
132
+ const MAX_BUFFERED_PAYLOAD_BYTES = (
133
+ Number.isFinite(configuredPayloadMb) && configuredPayloadMb > 0 ? configuredPayloadMb : 64
134
+ ) * 1024 * 1024;
135
+ const APPROVAL_TTL_MS = TTL_MS;
136
+ const TELEMETRY_TTL_MS = 30 * 60 * 1000;
137
+ const VIOLATION_TTL_MS = 24 * 60 * 60 * 1000;
129
138
  const BROKER_SECRET = process.env.BROKER_HMAC_SECRET || crypto.randomBytes(32).toString('hex');
130
139
 
131
140
  const store = new Map(); // uid → { iv, ciphertext, salt, createdAt, clients: [], approvals: [], hostToken, resolveCount }
141
+ let bufferedPayloadBytes = 0;
132
142
 
133
143
  // ─── Security Helpers ────────────────────────────────────────
134
144
  const SAFE_PARAM = /^[a-zA-Z0-9_-]{1,64}$/;
@@ -161,22 +171,61 @@ function isEncryptedPayload(body) {
161
171
  && /^[A-Za-z0-9+/]+={0,2}$/.test(body.ciphertext);
162
172
  }
163
173
 
174
+ function encryptedPayloadBytes(payload) {
175
+ return Buffer.byteLength(payload?.ciphertext || '')
176
+ + Buffer.byteLength(payload?.iv || '')
177
+ + Buffer.byteLength(payload?.salt || '');
178
+ }
179
+
180
+ function entryPayloadBytes(entry) {
181
+ if (!entry) return 0;
182
+ return encryptedPayloadBytes(entry)
183
+ + entry.clients.reduce((sum, item) => sum + encryptedPayloadBytes(item), 0)
184
+ + entry.approvals.reduce((sum, item) => sum + encryptedPayloadBytes(item), 0);
185
+ }
186
+
187
+ function deleteStoreEntry(uid) {
188
+ const entry = store.get(uid);
189
+ if (!entry) return false;
190
+ bufferedPayloadBytes = Math.max(0, bufferedPayloadBytes - entryPayloadBytes(entry));
191
+ return store.delete(uid);
192
+ }
193
+
164
194
  function pruneExpired() {
165
195
  const now = Date.now();
166
196
  for (const [uid, entry] of store) {
167
197
  if (now - entry.createdAt > TTL_MS) {
168
- store.delete(uid);
198
+ deleteStoreEntry(uid);
169
199
  console.log(`🗑️ Expired UID: ${uid}`);
200
+ continue;
170
201
  }
202
+ const previousBytes = entryPayloadBytes(entry);
203
+ entry.approvals = entry.approvals
204
+ .filter(item => now - item.createdAt <= APPROVAL_TTL_MS)
205
+ .slice(-MAX_APPROVALS_PER_UID);
206
+ entry.clients = entry.clients
207
+ .filter(item => now - item.seenAt <= TELEMETRY_TTL_MS)
208
+ .slice(-MAX_CLIENTS_PER_UID);
209
+ bufferedPayloadBytes = Math.max(0, bufferedPayloadBytes - previousBytes + entryPayloadBytes(entry));
171
210
  }
172
211
 
173
- // Strict check to prevent malicious OOM overflow
174
- if (ipViolations.size > MAX_VIOLATIONS) ipViolations.clear();
175
- if (blacklistedIPs.size > MAX_VIOLATIONS) blacklistedIPs.clear();
212
+ for (const [ip, violation] of ipViolations) {
213
+ if (now - violation.lastSeen > VIOLATION_TTL_MS) ipViolations.delete(ip);
214
+ }
215
+ for (const [ip, bannedAt] of blacklistedIPs) {
216
+ if (now - bannedAt > VIOLATION_TTL_MS) blacklistedIPs.delete(ip);
217
+ }
218
+ while (ipViolations.size > MAX_VIOLATIONS) {
219
+ ipViolations.delete(ipViolations.keys().next().value);
220
+ }
221
+ while (blacklistedIPs.size > MAX_VIOLATIONS) {
222
+ blacklistedIPs.delete(blacklistedIPs.keys().next().value);
223
+ }
176
224
  }
177
225
 
178
226
  // Run pruning every 5 minutes
179
- setInterval(pruneExpired, 5 * 60 * 1000);
227
+ const pruneTimer = setInterval(pruneExpired, 5 * 60 * 1000);
228
+ pruneTimer.unref?.();
180
229
 
181
230
  // ─── Routes ──────────────────────────────────────────────────
182
231
 
@@ -227,6 +276,11 @@ app.post('/register', strictLimiter, (req, res) => {
227
276
  if (store.size >= MAX_UIDS && !store.has(uid)) {
228
277
  return res.status(503).json({ error: 'Broker is at maximum capacity. Please try again later.' });
229
278
  }
279
+ const incomingBytes = encryptedPayloadBytes({ iv, ciphertext, salt });
280
+ const replacedBytes = entryPayloadBytes(store.get(uid));
281
+ if (bufferedPayloadBytes - replacedBytes + incomingBytes > MAX_BUFFERED_PAYLOAD_BYTES) {
282
+ return res.status(503).json({ error: 'Broker encrypted payload capacity reached. Please try again later.' });
283
+ }
230
284
 
231
285
  // Use provided host token if valid; otherwise generate a fresh one
232
286
  const hostToken = (typeof providedHostToken === 'string' && HOST_TOKEN_FORMAT.test(providedHostToken))
@@ -234,6 +288,7 @@ app.post('/register', strictLimiter, (req, res) => {
234
288
  : generateHostToken(uid + Date.now().toString());
235
289
 
236
290
  // Store the encrypted blob as-is — broker NEVER decrypts
291
+ if (store.has(uid)) deleteStoreEntry(uid);
237
292
  store.set(uid, {
238
293
  iv,
239
294
  ciphertext,
@@ -246,6 +301,7 @@ app.post('/register', strictLimiter, (req, res) => {
246
301
  hostToken,
247
302
  resolveCount: 0,
248
303
  });
304
+ bufferedPayloadBytes += incomingBytes;
249
305
 
250
306
  console.log(`✅ [${new Date().toLocaleTimeString()}] Registered UID: ${uid} (encrypted, ${ciphertext.length} bytes)`);
251
307
  // Return host token — this is the ONLY time it's sent; host must store it
@@ -277,19 +333,19 @@ app.get('/resolve/:uid', generalLimiter, (req, res) => {
277
333
 
278
334
  // Check TTL
279
335
  if (Date.now() - entry.createdAt > TTL_MS) {
280
- store.delete(uid);
336
+ deleteStoreEntry(uid);
281
337
  return res.status(410).json({ error: 'UID expired' });
282
338
  }
283
339
 
284
340
  // Enforce one-time sharing: check BEFORE sending response (prevents race condition)
285
341
  if (entry.oneTime && entry.resolveCount > 0) {
286
- store.delete(uid);
342
+ deleteStoreEntry(uid);
287
343
  return res.status(410).json({ error: 'One-time session already consumed' });
288
344
  }
289
345
 
290
346
  // Anti-scraping: cap total resolves per UID
291
347
  if (entry.resolveCount >= MAX_RESOLVES_PER_UID) {
292
- store.delete(uid);
348
+ deleteStoreEntry(uid);
293
349
  return res.status(429).json({ error: 'Resolve limit exceeded — session revoked for security' });
294
350
  }
295
351
 
@@ -315,7 +371,7 @@ app.get('/resolve/:uid', generalLimiter, (req, res) => {
315
371
 
316
372
  // Enforce one-time sharing: auto-delete AFTER response sent
317
373
  if (entry.oneTime) {
318
- store.delete(uid);
374
+ deleteStoreEntry(uid);
319
375
  console.log(`🔒 [${new Date().toLocaleTimeString()}] One-time UID ${uid} auto-revoked after first resolve`);
320
376
  }
321
377
  } catch (err) {
@@ -354,8 +410,18 @@ app.post('/approval-request/:uid', generalLimiter, (req, res) => {
354
410
  decidedAt: entry.approvalRequired ? null : Date.now(),
355
411
  };
356
412
 
413
+ const requestBytes = encryptedPayloadBytes(request);
414
+ const replacedBytes = entry.approvals.length >= MAX_APPROVALS_PER_UID
415
+ ? encryptedPayloadBytes(entry.approvals[0])
416
+ : 0;
417
+ if (bufferedPayloadBytes - replacedBytes + requestBytes > MAX_BUFFERED_PAYLOAD_BYTES) {
418
+ return res.status(503).json({ error: 'Broker encrypted payload capacity reached. Please try again later.' });
419
+ }
357
420
  entry.approvals.push(request);
358
- if (entry.approvals.length > 50) entry.approvals.shift();
421
+ bufferedPayloadBytes += requestBytes;
422
+ if (entry.approvals.length > MAX_APPROVALS_PER_UID) {
423
+ bufferedPayloadBytes -= encryptedPayloadBytes(entry.approvals.shift());
424
+ }
359
425
  res.json({ requestId: id, status: request.status, approvalRequired: entry.approvalRequired });
360
426
  } catch {
361
427
  res.status(500).json({ error: 'Internal server error' });
@@ -426,10 +492,21 @@ app.post('/client-info/:uid', generalLimiter, (req, res) => {
426
492
  return res.status(400).json({ error: 'Invalid encrypted telemetry payload' });
427
493
  }
428
494
 
429
- entry.clients.push({ iv: req.body.iv, ciphertext: req.body.ciphertext, salt: req.body.salt, seenAt: Date.now() });
495
+ const clientRecord = { iv: req.body.iv, ciphertext: req.body.ciphertext, salt: req.body.salt, seenAt: Date.now() };
496
+ const clientBytes = encryptedPayloadBytes(clientRecord);
497
+ const replacedBytes = entry.clients.length >= MAX_CLIENTS_PER_UID
498
+ ? encryptedPayloadBytes(entry.clients[0])
499
+ : 0;
500
+ if (bufferedPayloadBytes - replacedBytes + clientBytes > MAX_BUFFERED_PAYLOAD_BYTES) {
501
+ return res.status(503).json({ error: 'Broker encrypted payload capacity reached. Please try again later.' });
502
+ }
503
+ entry.clients.push(clientRecord);
504
+ bufferedPayloadBytes += clientBytes;
430
505
 
431
506
  // Keep max 50 recent client pings to prevent memory leaks
432
- if (entry.clients.length > 50) entry.clients.shift();
507
+ if (entry.clients.length > MAX_CLIENTS_PER_UID) {
508
+ bufferedPayloadBytes -= encryptedPayloadBytes(entry.clients.shift());
509
+ }
433
510
 
434
511
  res.json({ status: 'ok' });
435
512
  } catch (err) {
@@ -470,7 +547,7 @@ app.delete('/revoke/:uid', strictLimiter, (req, res) => {
470
547
  const entry = store.get(uid);
471
548
  if (!entry) return res.json({ status: 'not_found' });
472
549
  if (!requireHostToken(req, res, entry)) return;
473
- store.delete(uid);
550
+ deleteStoreEntry(uid);
474
551
  console.log(`🚫 [${new Date().toLocaleTimeString()}] Revoked UID: ${uid} (authenticated)`);
475
552
  res.json({ status: 'revoked' });
476
553
  });