@push.rocks/smartproxy 19.6.13 → 19.6.15

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.
Files changed (32) hide show
  1. package/dist_ts/core/utils/log-deduplicator.d.ts +39 -0
  2. package/dist_ts/core/utils/log-deduplicator.js +297 -0
  3. package/dist_ts/core/utils/shared-security-manager.d.ts +2 -1
  4. package/dist_ts/core/utils/shared-security-manager.js +22 -2
  5. package/dist_ts/proxies/http-proxy/http-proxy.d.ts +1 -0
  6. package/dist_ts/proxies/http-proxy/http-proxy.js +94 -9
  7. package/dist_ts/proxies/http-proxy/models/types.d.ts +2 -0
  8. package/dist_ts/proxies/http-proxy/models/types.js +1 -1
  9. package/dist_ts/proxies/http-proxy/security-manager.d.ts +42 -1
  10. package/dist_ts/proxies/http-proxy/security-manager.js +121 -2
  11. package/dist_ts/proxies/smart-proxy/connection-manager.d.ts +14 -0
  12. package/dist_ts/proxies/smart-proxy/connection-manager.js +86 -32
  13. package/dist_ts/proxies/smart-proxy/http-proxy-bridge.js +5 -1
  14. package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +1 -0
  15. package/dist_ts/proxies/smart-proxy/route-connection-handler.js +25 -9
  16. package/dist_ts/proxies/smart-proxy/security-manager.d.ts +9 -0
  17. package/dist_ts/proxies/smart-proxy/security-manager.js +63 -1
  18. package/dist_ts/proxies/smart-proxy/smart-proxy.js +4 -1
  19. package/package.json +1 -1
  20. package/readme.hints.md +113 -1
  21. package/readme.plan.md +34 -353
  22. package/ts/core/utils/log-deduplicator.ts +361 -0
  23. package/ts/core/utils/shared-security-manager.ts +24 -1
  24. package/ts/proxies/http-proxy/http-proxy.ts +129 -9
  25. package/ts/proxies/http-proxy/models/types.ts +4 -0
  26. package/ts/proxies/http-proxy/security-manager.ts +136 -1
  27. package/ts/proxies/smart-proxy/connection-manager.ts +113 -36
  28. package/ts/proxies/smart-proxy/http-proxy-bridge.ts +5 -0
  29. package/ts/proxies/smart-proxy/models/interfaces.ts +1 -0
  30. package/ts/proxies/smart-proxy/route-connection-handler.ts +52 -15
  31. package/ts/proxies/smart-proxy/security-manager.ts +76 -1
  32. package/ts/proxies/smart-proxy/smart-proxy.ts +4 -0
@@ -0,0 +1,361 @@
1
+ import { logger } from './logger.js';
2
+
3
+ interface ILogEvent {
4
+ level: 'info' | 'warn' | 'error' | 'debug';
5
+ message: string;
6
+ data?: any;
7
+ count: number;
8
+ firstSeen: number;
9
+ lastSeen: number;
10
+ }
11
+
12
+ interface IAggregatedEvent {
13
+ key: string;
14
+ events: Map<string, ILogEvent>;
15
+ flushTimer?: NodeJS.Timeout;
16
+ }
17
+
18
+ /**
19
+ * Log deduplication utility to reduce log spam for repetitive events
20
+ */
21
+ export class LogDeduplicator {
22
+ private globalFlushTimer?: NodeJS.Timeout;
23
+ private aggregatedEvents: Map<string, IAggregatedEvent> = new Map();
24
+ private flushInterval: number = 5000; // 5 seconds
25
+ private maxBatchSize: number = 100;
26
+ private rapidEventThreshold: number = 50; // Flush early if this many events in 1 second
27
+ private lastRapidCheck: number = Date.now();
28
+
29
+ constructor(flushInterval?: number) {
30
+ if (flushInterval) {
31
+ this.flushInterval = flushInterval;
32
+ }
33
+
34
+ // Set up global periodic flush to ensure logs are emitted regularly
35
+ this.globalFlushTimer = setInterval(() => {
36
+ this.flushAll();
37
+ }, this.flushInterval * 2); // Flush everything every 2x the normal interval
38
+
39
+ if (this.globalFlushTimer.unref) {
40
+ this.globalFlushTimer.unref();
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Log a deduplicated event
46
+ * @param key - Aggregation key (e.g., 'connection-rejected', 'cleanup-batch')
47
+ * @param level - Log level
48
+ * @param message - Log message template
49
+ * @param data - Additional data
50
+ * @param dedupeKey - Deduplication key within the aggregation (e.g., IP address, reason)
51
+ */
52
+ public log(
53
+ key: string,
54
+ level: 'info' | 'warn' | 'error' | 'debug',
55
+ message: string,
56
+ data?: any,
57
+ dedupeKey?: string
58
+ ): void {
59
+ const eventKey = dedupeKey || message;
60
+ const now = Date.now();
61
+
62
+ if (!this.aggregatedEvents.has(key)) {
63
+ this.aggregatedEvents.set(key, {
64
+ key,
65
+ events: new Map(),
66
+ flushTimer: undefined
67
+ });
68
+ }
69
+
70
+ const aggregated = this.aggregatedEvents.get(key)!;
71
+
72
+ if (aggregated.events.has(eventKey)) {
73
+ const event = aggregated.events.get(eventKey)!;
74
+ event.count++;
75
+ event.lastSeen = now;
76
+ if (data) {
77
+ event.data = { ...event.data, ...data };
78
+ }
79
+ } else {
80
+ aggregated.events.set(eventKey, {
81
+ level,
82
+ message,
83
+ data,
84
+ count: 1,
85
+ firstSeen: now,
86
+ lastSeen: now
87
+ });
88
+ }
89
+
90
+ // Check for rapid events (many events in short time)
91
+ const totalEvents = Array.from(aggregated.events.values()).reduce((sum, e) => sum + e.count, 0);
92
+
93
+ // If we're getting flooded with events, flush more frequently
94
+ if (now - this.lastRapidCheck < 1000 && totalEvents >= this.rapidEventThreshold) {
95
+ this.flush(key);
96
+ this.lastRapidCheck = now;
97
+ } else if (aggregated.events.size >= this.maxBatchSize) {
98
+ // Check if we should flush due to size
99
+ this.flush(key);
100
+ } else if (!aggregated.flushTimer) {
101
+ // Schedule flush
102
+ aggregated.flushTimer = setTimeout(() => {
103
+ this.flush(key);
104
+ }, this.flushInterval);
105
+
106
+ if (aggregated.flushTimer.unref) {
107
+ aggregated.flushTimer.unref();
108
+ }
109
+ }
110
+
111
+ // Update rapid check time
112
+ if (now - this.lastRapidCheck >= 1000) {
113
+ this.lastRapidCheck = now;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Flush aggregated events for a specific key
119
+ */
120
+ public flush(key: string): void {
121
+ const aggregated = this.aggregatedEvents.get(key);
122
+ if (!aggregated || aggregated.events.size === 0) {
123
+ return;
124
+ }
125
+
126
+ if (aggregated.flushTimer) {
127
+ clearTimeout(aggregated.flushTimer);
128
+ aggregated.flushTimer = undefined;
129
+ }
130
+
131
+ // Emit aggregated log based on the key
132
+ switch (key) {
133
+ case 'connection-rejected':
134
+ this.flushConnectionRejections(aggregated);
135
+ break;
136
+ case 'connection-cleanup':
137
+ this.flushConnectionCleanups(aggregated);
138
+ break;
139
+ case 'connection-terminated':
140
+ this.flushConnectionTerminations(aggregated);
141
+ break;
142
+ case 'ip-rejected':
143
+ this.flushIPRejections(aggregated);
144
+ break;
145
+ default:
146
+ this.flushGeneric(aggregated);
147
+ }
148
+
149
+ // Clear events
150
+ aggregated.events.clear();
151
+ }
152
+
153
+ /**
154
+ * Flush all pending events
155
+ */
156
+ public flushAll(): void {
157
+ for (const key of this.aggregatedEvents.keys()) {
158
+ this.flush(key);
159
+ }
160
+ }
161
+
162
+ private flushConnectionRejections(aggregated: IAggregatedEvent): void {
163
+ const totalCount = Array.from(aggregated.events.values()).reduce((sum, e) => sum + e.count, 0);
164
+ const byReason = new Map<string, number>();
165
+
166
+ for (const [, event] of aggregated.events) {
167
+ const reason = event.data?.reason || 'unknown';
168
+ byReason.set(reason, (byReason.get(reason) || 0) + event.count);
169
+ }
170
+
171
+ const reasonSummary = Array.from(byReason.entries())
172
+ .sort((a, b) => b[1] - a[1])
173
+ .map(([reason, count]) => `${reason}: ${count}`)
174
+ .join(', ');
175
+
176
+ const duration = Date.now() - Math.min(...Array.from(aggregated.events.values()).map(e => e.firstSeen));
177
+ logger.log('warn', `[SUMMARY] Rejected ${totalCount} connections in ${Math.round(duration/1000)}s`, {
178
+ reasons: reasonSummary,
179
+ uniqueIPs: aggregated.events.size,
180
+ component: 'connection-dedup'
181
+ });
182
+ }
183
+
184
+ private flushConnectionCleanups(aggregated: IAggregatedEvent): void {
185
+ const totalCount = Array.from(aggregated.events.values()).reduce((sum, e) => sum + e.count, 0);
186
+ const byReason = new Map<string, number>();
187
+
188
+ for (const [, event] of aggregated.events) {
189
+ const reason = event.data?.reason || 'normal';
190
+ byReason.set(reason, (byReason.get(reason) || 0) + event.count);
191
+ }
192
+
193
+ const reasonSummary = Array.from(byReason.entries())
194
+ .sort((a, b) => b[1] - a[1])
195
+ .slice(0, 5) // Top 5 reasons
196
+ .map(([reason, count]) => `${reason}: ${count}`)
197
+ .join(', ');
198
+
199
+ logger.log('info', `Cleaned up ${totalCount} connections`, {
200
+ reasons: reasonSummary,
201
+ duration: Date.now() - Math.min(...Array.from(aggregated.events.values()).map(e => e.firstSeen)),
202
+ component: 'connection-dedup'
203
+ });
204
+ }
205
+
206
+ private flushConnectionTerminations(aggregated: IAggregatedEvent): void {
207
+ const totalCount = Array.from(aggregated.events.values()).reduce((sum, e) => sum + e.count, 0);
208
+ const byReason = new Map<string, number>();
209
+ const byIP = new Map<string, number>();
210
+ let lastActiveCount = 0;
211
+
212
+ for (const [, event] of aggregated.events) {
213
+ const reason = event.data?.reason || 'unknown';
214
+ const ip = event.data?.remoteIP || 'unknown';
215
+
216
+ byReason.set(reason, (byReason.get(reason) || 0) + event.count);
217
+
218
+ // Track by IP
219
+ if (ip !== 'unknown') {
220
+ byIP.set(ip, (byIP.get(ip) || 0) + event.count);
221
+ }
222
+
223
+ // Track the last active connection count
224
+ if (event.data?.activeConnections !== undefined) {
225
+ lastActiveCount = event.data.activeConnections;
226
+ }
227
+ }
228
+
229
+ const reasonSummary = Array.from(byReason.entries())
230
+ .sort((a, b) => b[1] - a[1])
231
+ .slice(0, 5) // Top 5 reasons
232
+ .map(([reason, count]) => `${reason}: ${count}`)
233
+ .join(', ');
234
+
235
+ // Show top IPs if there are many different ones
236
+ let ipInfo = '';
237
+ if (byIP.size > 3) {
238
+ const topIPs = Array.from(byIP.entries())
239
+ .sort((a, b) => b[1] - a[1])
240
+ .slice(0, 3)
241
+ .map(([ip, count]) => `${ip} (${count})`)
242
+ .join(', ');
243
+ ipInfo = `, from ${byIP.size} IPs (top: ${topIPs})`;
244
+ } else if (byIP.size > 0) {
245
+ ipInfo = `, IPs: ${Array.from(byIP.keys()).join(', ')}`;
246
+ }
247
+
248
+ const duration = Date.now() - Math.min(...Array.from(aggregated.events.values()).map(e => e.firstSeen));
249
+
250
+ // Special handling for localhost connections (HttpProxy)
251
+ const localhostCount = byIP.get('::ffff:127.0.0.1') || 0;
252
+ if (localhostCount > 0 && byIP.size === 1) {
253
+ // All connections are from localhost (HttpProxy)
254
+ logger.log('info', `[SUMMARY] ${totalCount} HttpProxy connections terminated in ${Math.round(duration/1000)}s`, {
255
+ reasons: reasonSummary,
256
+ activeConnections: lastActiveCount,
257
+ component: 'connection-dedup'
258
+ });
259
+ } else {
260
+ logger.log('info', `[SUMMARY] ${totalCount} connections terminated in ${Math.round(duration/1000)}s`, {
261
+ reasons: reasonSummary,
262
+ activeConnections: lastActiveCount,
263
+ uniqueReasons: byReason.size,
264
+ ...(ipInfo ? { ips: ipInfo } : {}),
265
+ component: 'connection-dedup'
266
+ });
267
+ }
268
+ }
269
+
270
+ private flushIPRejections(aggregated: IAggregatedEvent): void {
271
+ const byIP = new Map<string, { count: number; reasons: Set<string> }>();
272
+
273
+ for (const [ip, event] of aggregated.events) {
274
+ if (!byIP.has(ip)) {
275
+ byIP.set(ip, { count: 0, reasons: new Set() });
276
+ }
277
+ const ipData = byIP.get(ip)!;
278
+ ipData.count += event.count;
279
+ if (event.data?.reason) {
280
+ ipData.reasons.add(event.data.reason);
281
+ }
282
+ }
283
+
284
+ // Log top offenders
285
+ const topOffenders = Array.from(byIP.entries())
286
+ .sort((a, b) => b[1].count - a[1].count)
287
+ .slice(0, 10)
288
+ .map(([ip, data]) => `${ip} (${data.count}x, ${Array.from(data.reasons).join('/')})`)
289
+ .join(', ');
290
+
291
+ const totalRejections = Array.from(byIP.values()).reduce((sum, data) => sum + data.count, 0);
292
+
293
+ const duration = Date.now() - Math.min(...Array.from(aggregated.events.values()).map(e => e.firstSeen));
294
+ logger.log('warn', `[SUMMARY] Rejected ${totalRejections} connections from ${byIP.size} IPs in ${Math.round(duration/1000)}s`, {
295
+ topOffenders,
296
+ component: 'ip-dedup'
297
+ });
298
+ }
299
+
300
+ private flushGeneric(aggregated: IAggregatedEvent): void {
301
+ const totalCount = Array.from(aggregated.events.values()).reduce((sum, e) => sum + e.count, 0);
302
+ const level = aggregated.events.values().next().value?.level || 'info';
303
+
304
+ // Special handling for IP cleanup events
305
+ if (aggregated.key === 'ip-cleanup') {
306
+ const totalCleaned = Array.from(aggregated.events.values()).reduce((sum, e) => {
307
+ return sum + (e.data?.cleanedIPs || 0) + (e.data?.cleanedRateLimits || 0);
308
+ }, 0);
309
+
310
+ if (totalCleaned > 0) {
311
+ logger.log(level as any, `IP tracking cleanup: removed ${totalCleaned} entries across ${totalCount} cleanup cycles`, {
312
+ duration: Date.now() - Math.min(...Array.from(aggregated.events.values()).map(e => e.firstSeen)),
313
+ component: 'log-dedup'
314
+ });
315
+ }
316
+ } else {
317
+ logger.log(level as any, `${aggregated.key}: ${totalCount} events`, {
318
+ uniqueEvents: aggregated.events.size,
319
+ duration: Date.now() - Math.min(...Array.from(aggregated.events.values()).map(e => e.firstSeen)),
320
+ component: 'log-dedup'
321
+ });
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Cleanup and stop deduplication
327
+ */
328
+ public cleanup(): void {
329
+ this.flushAll();
330
+
331
+ if (this.globalFlushTimer) {
332
+ clearInterval(this.globalFlushTimer);
333
+ this.globalFlushTimer = undefined;
334
+ }
335
+
336
+ for (const aggregated of this.aggregatedEvents.values()) {
337
+ if (aggregated.flushTimer) {
338
+ clearTimeout(aggregated.flushTimer);
339
+ }
340
+ }
341
+ this.aggregatedEvents.clear();
342
+ }
343
+ }
344
+
345
+ // Global instance for connection-related log deduplication
346
+ export const connectionLogDeduplicator = new LogDeduplicator(5000); // 5 second batches
347
+
348
+ // Ensure logs are flushed on process exit
349
+ process.on('beforeExit', () => {
350
+ connectionLogDeduplicator.flushAll();
351
+ });
352
+
353
+ process.on('SIGINT', () => {
354
+ connectionLogDeduplicator.cleanup();
355
+ process.exit(0);
356
+ });
357
+
358
+ process.on('SIGTERM', () => {
359
+ connectionLogDeduplicator.cleanup();
360
+ process.exit(0);
361
+ });
@@ -152,9 +152,10 @@ export class SharedSecurityManager {
152
152
  *
153
153
  * @param route - The route to check
154
154
  * @param context - The request context
155
+ * @param routeConnectionCount - Current connection count for this route (optional)
155
156
  * @returns Whether access is allowed
156
157
  */
157
- public isAllowed(route: IRouteConfig, context: IRouteContext): boolean {
158
+ public isAllowed(route: IRouteConfig, context: IRouteContext, routeConnectionCount?: number): boolean {
158
159
  if (!route.security) {
159
160
  return true; // No security restrictions
160
161
  }
@@ -165,6 +166,14 @@ export class SharedSecurityManager {
165
166
  return false;
166
167
  }
167
168
 
169
+ // --- Route-level connection limit ---
170
+ if (route.security.maxConnections !== undefined && routeConnectionCount !== undefined) {
171
+ if (routeConnectionCount >= route.security.maxConnections) {
172
+ this.logger?.debug?.(`Route connection limit (${route.security.maxConnections}) exceeded for route ${route.name || 'unnamed'}`);
173
+ return false;
174
+ }
175
+ }
176
+
168
177
  // --- Rate limiting ---
169
178
  if (route.security.rateLimit?.enabled && !this.isWithinRateLimit(route, context)) {
170
179
  this.logger?.debug?.(`Rate limit exceeded for route ${route.name || 'unnamed'}`);
@@ -304,6 +313,20 @@ export class SharedSecurityManager {
304
313
  // Clean up rate limits
305
314
  cleanupExpiredRateLimits(this.rateLimits, this.logger);
306
315
 
316
+ // Clean up IP connection tracking
317
+ let cleanedIPs = 0;
318
+ for (const [ip, info] of this.connectionsByIP.entries()) {
319
+ // Remove IPs with no active connections and no recent timestamps
320
+ if (info.connections.size === 0 && info.timestamps.length === 0) {
321
+ this.connectionsByIP.delete(ip);
322
+ cleanedIPs++;
323
+ }
324
+ }
325
+
326
+ if (cleanedIPs > 0 && this.logger?.debug) {
327
+ this.logger.debug(`Cleaned up ${cleanedIPs} IPs with no active connections`);
328
+ }
329
+
307
330
  // IP filter cache doesn't need cleanup (tied to routes)
308
331
  }
309
332
 
@@ -17,6 +17,8 @@ import { WebSocketHandler } from './websocket-handler.js';
17
17
  import { HttpRouter } from '../../routing/router/index.js';
18
18
  import { cleanupSocket } from '../../core/utils/socket-utils.js';
19
19
  import { FunctionCache } from './function-cache.js';
20
+ import { SecurityManager } from './security-manager.js';
21
+ import { connectionLogDeduplicator } from '../../core/utils/log-deduplicator.js';
20
22
 
21
23
  /**
22
24
  * HttpProxy provides a reverse proxy with TLS termination, WebSocket support,
@@ -43,6 +45,7 @@ export class HttpProxy implements IMetricsTracker {
43
45
  private router = new HttpRouter(); // Unified HTTP router
44
46
  private routeManager: RouteManager;
45
47
  private functionCache: FunctionCache;
48
+ private securityManager: SecurityManager;
46
49
 
47
50
  // State tracking
48
51
  public socketMap = new plugins.lik.ObjectMap<plugins.net.Socket>();
@@ -113,6 +116,14 @@ export class HttpProxy implements IMetricsTracker {
113
116
  maxCacheSize: this.options.functionCacheSize || 1000,
114
117
  defaultTtl: this.options.functionCacheTtl || 5000
115
118
  });
119
+
120
+ // Initialize security manager
121
+ this.securityManager = new SecurityManager(
122
+ this.logger,
123
+ [],
124
+ this.options.maxConnectionsPerIP || 100,
125
+ this.options.connectionRateLimitPerMinute || 300
126
+ );
116
127
 
117
128
  // Initialize other components
118
129
  this.certificateManager = new CertificateManager(this.options);
@@ -269,14 +280,113 @@ export class HttpProxy implements IMetricsTracker {
269
280
  */
270
281
  private setupConnectionTracking(): void {
271
282
  this.httpsServer.on('connection', (connection: plugins.net.Socket) => {
272
- // Check if max connections reached
283
+ let remoteIP = connection.remoteAddress || '';
284
+ const connectionId = Math.random().toString(36).substring(2, 15);
285
+ const isFromSmartProxy = this.options.portProxyIntegration && connection.remoteAddress?.includes('127.0.0.1');
286
+
287
+ // For SmartProxy connections, wait for CLIENT_IP header
288
+ if (isFromSmartProxy) {
289
+ let headerBuffer = Buffer.alloc(0);
290
+ let headerParsed = false;
291
+
292
+ const parseHeader = (data: Buffer) => {
293
+ if (headerParsed) return data;
294
+
295
+ headerBuffer = Buffer.concat([headerBuffer, data]);
296
+ const headerStr = headerBuffer.toString();
297
+ const headerEnd = headerStr.indexOf('\r\n');
298
+
299
+ if (headerEnd !== -1) {
300
+ const header = headerStr.substring(0, headerEnd);
301
+ if (header.startsWith('CLIENT_IP:')) {
302
+ remoteIP = header.substring(10); // Extract IP after "CLIENT_IP:"
303
+ this.logger.debug(`Extracted client IP from SmartProxy: ${remoteIP}`);
304
+ }
305
+ headerParsed = true;
306
+
307
+ // Store the real IP on the connection
308
+ (connection as any)._realRemoteIP = remoteIP;
309
+
310
+ // Validate the real IP
311
+ const ipValidation = this.securityManager.validateIP(remoteIP);
312
+ if (!ipValidation.allowed) {
313
+ connectionLogDeduplicator.log(
314
+ 'ip-rejected',
315
+ 'warn',
316
+ `HttpProxy connection rejected (via SmartProxy)`,
317
+ { remoteIP, reason: ipValidation.reason, component: 'http-proxy' },
318
+ remoteIP
319
+ );
320
+ connection.destroy();
321
+ return null;
322
+ }
323
+
324
+ // Track connection by real IP
325
+ this.securityManager.trackConnectionByIP(remoteIP, connectionId);
326
+
327
+ // Return remaining data after header
328
+ return headerBuffer.slice(headerEnd + 2);
329
+ }
330
+ return null;
331
+ };
332
+
333
+ // Override the first data handler to parse header
334
+ const originalEmit = connection.emit;
335
+ connection.emit = function(event: string, ...args: any[]) {
336
+ if (event === 'data' && !headerParsed) {
337
+ const remaining = parseHeader(args[0]);
338
+ if (remaining && remaining.length > 0) {
339
+ // Call original emit with remaining data
340
+ return originalEmit.apply(connection, ['data', remaining]);
341
+ } else if (headerParsed) {
342
+ // Header parsed but no remaining data
343
+ return true;
344
+ }
345
+ // Header not complete yet, suppress this data event
346
+ return true;
347
+ }
348
+ return originalEmit.apply(connection, [event, ...args]);
349
+ } as any;
350
+ } else {
351
+ // Direct connection - validate immediately
352
+ const ipValidation = this.securityManager.validateIP(remoteIP);
353
+ if (!ipValidation.allowed) {
354
+ connectionLogDeduplicator.log(
355
+ 'ip-rejected',
356
+ 'warn',
357
+ `HttpProxy connection rejected`,
358
+ { remoteIP, reason: ipValidation.reason, component: 'http-proxy' },
359
+ remoteIP
360
+ );
361
+ connection.destroy();
362
+ return;
363
+ }
364
+
365
+ // Track connection by IP
366
+ this.securityManager.trackConnectionByIP(remoteIP, connectionId);
367
+ }
368
+
369
+ // Then check global max connections
273
370
  if (this.socketMap.getArray().length >= this.options.maxConnections) {
274
- this.logger.warn(`Max connections (${this.options.maxConnections}) reached, rejecting new connection`);
371
+ connectionLogDeduplicator.log(
372
+ 'connection-rejected',
373
+ 'warn',
374
+ 'HttpProxy max connections reached',
375
+ {
376
+ reason: 'global-limit',
377
+ currentConnections: this.socketMap.getArray().length,
378
+ maxConnections: this.options.maxConnections,
379
+ component: 'http-proxy'
380
+ },
381
+ 'http-proxy-global-limit'
382
+ );
275
383
  connection.destroy();
276
384
  return;
277
385
  }
278
-
279
- // Add connection to tracking
386
+
387
+ // Add connection to tracking with metadata
388
+ (connection as any)._connectionId = connectionId;
389
+ (connection as any)._remoteIP = remoteIP;
280
390
  this.socketMap.add(connection);
281
391
  this.connectedClients = this.socketMap.getArray().length;
282
392
 
@@ -284,12 +394,12 @@ export class HttpProxy implements IMetricsTracker {
284
394
  const localPort = connection.localPort || 0;
285
395
  const remotePort = connection.remotePort || 0;
286
396
 
287
- // If this connection is from a SmartProxy (usually indicated by it coming from localhost)
288
- if (this.options.portProxyIntegration && connection.remoteAddress?.includes('127.0.0.1')) {
397
+ // If this connection is from a SmartProxy
398
+ if (isFromSmartProxy) {
289
399
  this.portProxyConnections++;
290
- this.logger.debug(`New connection from SmartProxy (local: ${localPort}, remote: ${remotePort})`);
400
+ this.logger.debug(`New connection from SmartProxy for client ${remoteIP} (local: ${localPort}, remote: ${remotePort})`);
291
401
  } else {
292
- this.logger.debug(`New direct connection (local: ${localPort}, remote: ${remotePort})`);
402
+ this.logger.debug(`New direct connection from ${remoteIP} (local: ${localPort}, remote: ${remotePort})`);
293
403
  }
294
404
 
295
405
  // Setup connection cleanup handlers
@@ -298,12 +408,19 @@ export class HttpProxy implements IMetricsTracker {
298
408
  this.socketMap.remove(connection);
299
409
  this.connectedClients = this.socketMap.getArray().length;
300
410
 
411
+ // Remove IP tracking
412
+ const connId = (connection as any)._connectionId;
413
+ const connIP = (connection as any)._realRemoteIP || (connection as any)._remoteIP;
414
+ if (connId && connIP) {
415
+ this.securityManager.removeConnectionByIP(connIP, connId);
416
+ }
417
+
301
418
  // If this was a SmartProxy connection, decrement the counter
302
419
  if (this.options.portProxyIntegration && connection.remoteAddress?.includes('127.0.0.1')) {
303
420
  this.portProxyConnections--;
304
421
  }
305
422
 
306
- this.logger.debug(`Connection closed. ${this.connectedClients} connections remaining`);
423
+ this.logger.debug(`Connection closed from ${connIP || 'unknown'}. ${this.connectedClients} connections remaining`);
307
424
  }
308
425
  };
309
426
 
@@ -480,6 +597,9 @@ export class HttpProxy implements IMetricsTracker {
480
597
 
481
598
  // Certificate management cleanup is handled by SmartCertManager
482
599
 
600
+ // Flush any pending deduplicated logs
601
+ connectionLogDeduplicator.flushAll();
602
+
483
603
  // Close the HTTPS server
484
604
  return new Promise((resolve) => {
485
605
  this.httpsServer.close(() => {
@@ -45,6 +45,10 @@ export interface IHttpProxyOptions {
45
45
 
46
46
  // Direct route configurations
47
47
  routes?: IRouteConfig[];
48
+
49
+ // Rate limiting and security
50
+ maxConnectionsPerIP?: number; // Maximum simultaneous connections from a single IP
51
+ connectionRateLimitPerMinute?: number; // Max new connections per minute from a single IP
48
52
  }
49
53
 
50
54
  /**