crackerjack 0.31.10__py3-none-any.whl → 0.31.13__py3-none-any.whl

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.

Potentially problematic release.


This version of crackerjack might be problematic. Click here for more details.

Files changed (155) hide show
  1. crackerjack/CLAUDE.md +288 -705
  2. crackerjack/__main__.py +22 -8
  3. crackerjack/agents/__init__.py +0 -3
  4. crackerjack/agents/architect_agent.py +0 -43
  5. crackerjack/agents/base.py +1 -9
  6. crackerjack/agents/coordinator.py +2 -148
  7. crackerjack/agents/documentation_agent.py +109 -81
  8. crackerjack/agents/dry_agent.py +122 -97
  9. crackerjack/agents/formatting_agent.py +3 -16
  10. crackerjack/agents/import_optimization_agent.py +1174 -130
  11. crackerjack/agents/performance_agent.py +956 -188
  12. crackerjack/agents/performance_helpers.py +229 -0
  13. crackerjack/agents/proactive_agent.py +1 -48
  14. crackerjack/agents/refactoring_agent.py +516 -246
  15. crackerjack/agents/refactoring_helpers.py +282 -0
  16. crackerjack/agents/security_agent.py +393 -90
  17. crackerjack/agents/test_creation_agent.py +1776 -120
  18. crackerjack/agents/test_specialist_agent.py +59 -15
  19. crackerjack/agents/tracker.py +0 -102
  20. crackerjack/api.py +145 -37
  21. crackerjack/cli/handlers.py +48 -30
  22. crackerjack/cli/interactive.py +11 -11
  23. crackerjack/cli/options.py +66 -4
  24. crackerjack/code_cleaner.py +808 -148
  25. crackerjack/config/global_lock_config.py +110 -0
  26. crackerjack/config/hooks.py +43 -64
  27. crackerjack/core/async_workflow_orchestrator.py +247 -97
  28. crackerjack/core/autofix_coordinator.py +192 -109
  29. crackerjack/core/enhanced_container.py +46 -63
  30. crackerjack/core/file_lifecycle.py +549 -0
  31. crackerjack/core/performance.py +9 -8
  32. crackerjack/core/performance_monitor.py +395 -0
  33. crackerjack/core/phase_coordinator.py +281 -94
  34. crackerjack/core/proactive_workflow.py +9 -58
  35. crackerjack/core/resource_manager.py +501 -0
  36. crackerjack/core/service_watchdog.py +490 -0
  37. crackerjack/core/session_coordinator.py +4 -8
  38. crackerjack/core/timeout_manager.py +504 -0
  39. crackerjack/core/websocket_lifecycle.py +475 -0
  40. crackerjack/core/workflow_orchestrator.py +343 -209
  41. crackerjack/dynamic_config.py +50 -9
  42. crackerjack/errors.py +3 -4
  43. crackerjack/executors/async_hook_executor.py +63 -13
  44. crackerjack/executors/cached_hook_executor.py +14 -14
  45. crackerjack/executors/hook_executor.py +100 -37
  46. crackerjack/executors/hook_lock_manager.py +856 -0
  47. crackerjack/executors/individual_hook_executor.py +120 -86
  48. crackerjack/intelligence/__init__.py +0 -7
  49. crackerjack/intelligence/adaptive_learning.py +13 -86
  50. crackerjack/intelligence/agent_orchestrator.py +15 -78
  51. crackerjack/intelligence/agent_registry.py +12 -59
  52. crackerjack/intelligence/agent_selector.py +31 -92
  53. crackerjack/intelligence/integration.py +1 -41
  54. crackerjack/interactive.py +9 -9
  55. crackerjack/managers/async_hook_manager.py +25 -8
  56. crackerjack/managers/hook_manager.py +9 -9
  57. crackerjack/managers/publish_manager.py +57 -59
  58. crackerjack/managers/test_command_builder.py +6 -36
  59. crackerjack/managers/test_executor.py +9 -61
  60. crackerjack/managers/test_manager.py +17 -63
  61. crackerjack/managers/test_manager_backup.py +77 -127
  62. crackerjack/managers/test_progress.py +4 -23
  63. crackerjack/mcp/cache.py +5 -12
  64. crackerjack/mcp/client_runner.py +10 -10
  65. crackerjack/mcp/context.py +64 -6
  66. crackerjack/mcp/dashboard.py +14 -11
  67. crackerjack/mcp/enhanced_progress_monitor.py +55 -55
  68. crackerjack/mcp/file_monitor.py +72 -42
  69. crackerjack/mcp/progress_components.py +103 -84
  70. crackerjack/mcp/progress_monitor.py +122 -49
  71. crackerjack/mcp/rate_limiter.py +12 -12
  72. crackerjack/mcp/server_core.py +16 -22
  73. crackerjack/mcp/service_watchdog.py +26 -26
  74. crackerjack/mcp/state.py +15 -0
  75. crackerjack/mcp/tools/core_tools.py +95 -39
  76. crackerjack/mcp/tools/error_analyzer.py +6 -32
  77. crackerjack/mcp/tools/execution_tools.py +1 -56
  78. crackerjack/mcp/tools/execution_tools_backup.py +35 -131
  79. crackerjack/mcp/tools/intelligence_tool_registry.py +0 -36
  80. crackerjack/mcp/tools/intelligence_tools.py +2 -55
  81. crackerjack/mcp/tools/monitoring_tools.py +308 -145
  82. crackerjack/mcp/tools/proactive_tools.py +12 -42
  83. crackerjack/mcp/tools/progress_tools.py +23 -15
  84. crackerjack/mcp/tools/utility_tools.py +3 -40
  85. crackerjack/mcp/tools/workflow_executor.py +40 -60
  86. crackerjack/mcp/websocket/app.py +0 -3
  87. crackerjack/mcp/websocket/endpoints.py +206 -268
  88. crackerjack/mcp/websocket/jobs.py +213 -66
  89. crackerjack/mcp/websocket/server.py +84 -6
  90. crackerjack/mcp/websocket/websocket_handler.py +137 -29
  91. crackerjack/models/config_adapter.py +3 -16
  92. crackerjack/models/protocols.py +162 -3
  93. crackerjack/models/resource_protocols.py +454 -0
  94. crackerjack/models/task.py +3 -3
  95. crackerjack/monitoring/__init__.py +0 -0
  96. crackerjack/monitoring/ai_agent_watchdog.py +25 -71
  97. crackerjack/monitoring/regression_prevention.py +28 -87
  98. crackerjack/orchestration/advanced_orchestrator.py +44 -78
  99. crackerjack/orchestration/coverage_improvement.py +10 -60
  100. crackerjack/orchestration/execution_strategies.py +16 -16
  101. crackerjack/orchestration/test_progress_streamer.py +61 -53
  102. crackerjack/plugins/base.py +1 -1
  103. crackerjack/plugins/managers.py +22 -20
  104. crackerjack/py313.py +65 -21
  105. crackerjack/services/backup_service.py +467 -0
  106. crackerjack/services/bounded_status_operations.py +627 -0
  107. crackerjack/services/cache.py +7 -9
  108. crackerjack/services/config.py +35 -52
  109. crackerjack/services/config_integrity.py +5 -16
  110. crackerjack/services/config_merge.py +542 -0
  111. crackerjack/services/contextual_ai_assistant.py +17 -19
  112. crackerjack/services/coverage_ratchet.py +44 -73
  113. crackerjack/services/debug.py +25 -39
  114. crackerjack/services/dependency_monitor.py +52 -50
  115. crackerjack/services/enhanced_filesystem.py +14 -11
  116. crackerjack/services/file_hasher.py +1 -1
  117. crackerjack/services/filesystem.py +1 -12
  118. crackerjack/services/git.py +71 -47
  119. crackerjack/services/health_metrics.py +31 -27
  120. crackerjack/services/initialization.py +276 -428
  121. crackerjack/services/input_validator.py +760 -0
  122. crackerjack/services/log_manager.py +16 -16
  123. crackerjack/services/logging.py +7 -6
  124. crackerjack/services/metrics.py +43 -43
  125. crackerjack/services/pattern_cache.py +2 -31
  126. crackerjack/services/pattern_detector.py +26 -63
  127. crackerjack/services/performance_benchmarks.py +20 -45
  128. crackerjack/services/regex_patterns.py +2887 -0
  129. crackerjack/services/regex_utils.py +537 -0
  130. crackerjack/services/secure_path_utils.py +683 -0
  131. crackerjack/services/secure_status_formatter.py +534 -0
  132. crackerjack/services/secure_subprocess.py +605 -0
  133. crackerjack/services/security.py +47 -10
  134. crackerjack/services/security_logger.py +492 -0
  135. crackerjack/services/server_manager.py +109 -50
  136. crackerjack/services/smart_scheduling.py +8 -25
  137. crackerjack/services/status_authentication.py +603 -0
  138. crackerjack/services/status_security_manager.py +442 -0
  139. crackerjack/services/thread_safe_status_collector.py +546 -0
  140. crackerjack/services/tool_version_service.py +1 -23
  141. crackerjack/services/unified_config.py +36 -58
  142. crackerjack/services/validation_rate_limiter.py +269 -0
  143. crackerjack/services/version_checker.py +9 -40
  144. crackerjack/services/websocket_resource_limiter.py +572 -0
  145. crackerjack/slash_commands/__init__.py +52 -2
  146. crackerjack/tools/__init__.py +0 -0
  147. crackerjack/tools/validate_input_validator_patterns.py +262 -0
  148. crackerjack/tools/validate_regex_patterns.py +198 -0
  149. {crackerjack-0.31.10.dist-info → crackerjack-0.31.13.dist-info}/METADATA +197 -12
  150. crackerjack-0.31.13.dist-info/RECORD +178 -0
  151. crackerjack/cli/facade.py +0 -104
  152. crackerjack-0.31.10.dist-info/RECORD +0 -149
  153. {crackerjack-0.31.10.dist-info → crackerjack-0.31.13.dist-info}/WHEEL +0 -0
  154. {crackerjack-0.31.10.dist-info → crackerjack-0.31.13.dist-info}/entry_points.txt +0 -0
  155. {crackerjack-0.31.10.dist-info → crackerjack-0.31.13.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,572 @@
1
+ """
2
+ WebSocket Resource Limiter to prevent resource exhaustion attacks.
3
+
4
+ Provides comprehensive resource monitoring and limiting for WebSocket
5
+ connections, including connection limits, message limits, memory usage
6
+ tracking, and DoS prevention.
7
+ """
8
+
9
+ import asyncio
10
+ import time
11
+ import typing as t
12
+ from collections import defaultdict, deque
13
+ from dataclasses import dataclass, field
14
+ from threading import RLock
15
+
16
+ from .security_logger import SecurityEventLevel, SecurityEventType, get_security_logger
17
+
18
+
19
+ @dataclass
20
+ class ConnectionMetrics:
21
+ """Metrics for individual WebSocket connections."""
22
+
23
+ client_id: str
24
+ connect_time: float = field(default_factory=time.time)
25
+ message_count: int = 0
26
+ bytes_sent: int = 0
27
+ bytes_received: int = 0
28
+ last_activity: float = field(default_factory=time.time)
29
+
30
+ @property
31
+ def connection_duration(self) -> float:
32
+ """Get connection duration in seconds."""
33
+ return time.time() - self.connect_time
34
+
35
+ @property
36
+ def idle_time(self) -> float:
37
+ """Get idle time since last activity."""
38
+ return time.time() - self.last_activity
39
+
40
+
41
+ @dataclass
42
+ class ResourceLimits:
43
+ """Resource limits configuration."""
44
+
45
+ max_connections: int = 50
46
+ max_connections_per_ip: int = 10
47
+ max_message_size: int = 64 * 1024 # 64KB
48
+ max_messages_per_minute: int = 100
49
+ max_messages_per_connection: int = 10000
50
+ max_connection_duration: float = 3600.0 # 1 hour
51
+ max_idle_time: float = 300.0 # 5 minutes
52
+ max_memory_usage_mb: int = 100
53
+ connection_timeout: float = 30.0
54
+ message_timeout: float = 10.0
55
+
56
+
57
+ class ResourceExhaustedError(Exception):
58
+ """Raised when resource limits are exceeded."""
59
+
60
+ pass
61
+
62
+
63
+ class WebSocketResourceLimiter:
64
+ """
65
+ Resource limiter for WebSocket connections.
66
+
67
+ Features:
68
+ - Connection count and per-IP limits
69
+ - Message size and rate limiting
70
+ - Memory usage monitoring
71
+ - Connection duration limits
72
+ - Idle connection cleanup
73
+ - DoS attack prevention
74
+ """
75
+
76
+ def __init__(self, limits: ResourceLimits | None = None):
77
+ """
78
+ Initialize WebSocket resource limiter.
79
+
80
+ Args:
81
+ limits: Resource limits configuration
82
+ """
83
+ self.limits = limits or ResourceLimits()
84
+ self.security_logger = get_security_logger()
85
+
86
+ self._setup_limiter_components()
87
+
88
+ def _setup_limiter_components(self) -> None:
89
+ """Set up all limiter components in initialization."""
90
+ self._initialize_connection_tracking()
91
+ self._initialize_metrics_tracking()
92
+ self._initialize_cleanup_system()
93
+
94
+ def _initialize_connection_tracking(self) -> None:
95
+ """Initialize thread-safe connection tracking structures."""
96
+ self._lock = RLock()
97
+ self._connections: dict[str, ConnectionMetrics] = {}
98
+ self._ip_connections: dict[str, set[str]] = defaultdict(set)
99
+ self._message_history: dict[str, deque[t.Any]] = defaultdict(
100
+ lambda: deque(maxlen=100)
101
+ )
102
+ self._blocked_ips: dict[str, float] = {}
103
+
104
+ def _initialize_metrics_tracking(self) -> None:
105
+ """Initialize metrics tracking with proper typing."""
106
+ self._connection_metrics: dict[str, ConnectionMetrics] = {}
107
+ self._message_queues: dict[str, deque[bytes]] = defaultdict(deque)
108
+ self._resource_usage: dict[str, dict[str, t.Any]] = {}
109
+ self._memory_usage: int = 0
110
+
111
+ def _initialize_cleanup_system(self) -> None:
112
+ """Initialize the background cleanup system."""
113
+ self._cleanup_task: asyncio.Task[t.Any] | None = None
114
+ self._shutdown_event = asyncio.Event()
115
+
116
+ async def start(self) -> None:
117
+ """Start the resource limiter with background cleanup."""
118
+
119
+ if self._cleanup_task is None:
120
+ self._cleanup_task = asyncio.create_task(self._cleanup_loop())
121
+
122
+ self.security_logger.log_security_event(
123
+ event_type=SecurityEventType.SERVICE_START,
124
+ level=SecurityEventLevel.INFO,
125
+ message="WebSocket resource limiter started",
126
+ operation="limiter_start",
127
+ )
128
+
129
+ async def stop(self) -> None:
130
+ """Stop the resource limiter and cleanup resources."""
131
+
132
+ self._shutdown_event.set()
133
+
134
+ if self._cleanup_task:
135
+ try:
136
+ await asyncio.wait_for(self._cleanup_task, timeout=5.0)
137
+ except TimeoutError:
138
+ self._cleanup_task.cancel()
139
+ try:
140
+ await self._cleanup_task
141
+ except asyncio.CancelledError:
142
+ pass
143
+
144
+ self._cleanup_task = None
145
+
146
+ # Force cleanup all connections
147
+ with self._lock:
148
+ self._connections.clear()
149
+ self._ip_connections.clear()
150
+ self._message_history.clear()
151
+
152
+ self.security_logger.log_security_event(
153
+ event_type=SecurityEventType.SERVICE_STOP,
154
+ level=SecurityEventLevel.INFO,
155
+ message="WebSocket resource limiter stopped",
156
+ operation="limiter_stop",
157
+ )
158
+
159
+ def validate_new_connection(self, client_id: str, client_ip: str) -> None:
160
+ """
161
+ Validate if a new connection can be accepted.
162
+
163
+ Args:
164
+ client_id: Unique client identifier
165
+ client_ip: Client IP address
166
+
167
+ Raises:
168
+ ResourceExhaustedError: If connection limits exceeded
169
+ """
170
+
171
+ with self._lock:
172
+ current_time = time.time()
173
+
174
+ self._check_ip_blocking_status(client_ip, current_time)
175
+ self._check_total_connection_limit(client_id, client_ip)
176
+ self._check_per_ip_connection_limit(client_id, client_ip, current_time)
177
+
178
+ def _check_ip_blocking_status(self, client_ip: str, current_time: float) -> None:
179
+ """Check if IP is currently blocked."""
180
+ if client_ip in self._blocked_ips:
181
+ if current_time < self._blocked_ips[client_ip]:
182
+ raise ResourceExhaustedError(f"IP {client_ip} is temporarily blocked")
183
+ else:
184
+ del self._blocked_ips[client_ip]
185
+
186
+ def _check_total_connection_limit(self, client_id: str, client_ip: str) -> None:
187
+ """Check if total connection limit is exceeded."""
188
+ if len(self._connections) >= self.limits.max_connections:
189
+ self.security_logger.log_security_event(
190
+ event_type=SecurityEventType.RATE_LIMIT_EXCEEDED,
191
+ level=SecurityEventLevel.WARNING,
192
+ message=f"Max connections exceeded: {len(self._connections)}",
193
+ client_id=client_id,
194
+ operation="connection_validation",
195
+ additional_data={"client_ip": client_ip},
196
+ )
197
+ raise ResourceExhaustedError(
198
+ f"Maximum connections exceeded: {len(self._connections)}"
199
+ )
200
+
201
+ def _check_per_ip_connection_limit(
202
+ self, client_id: str, client_ip: str, current_time: float
203
+ ) -> None:
204
+ """Check if per-IP connection limit is exceeded."""
205
+ ip_connection_count = len(self._ip_connections[client_ip])
206
+ if ip_connection_count >= self.limits.max_connections_per_ip:
207
+ self.security_logger.log_security_event(
208
+ event_type=SecurityEventType.RATE_LIMIT_EXCEEDED,
209
+ level=SecurityEventLevel.HIGH,
210
+ message=f"Max connections per IP exceeded: {ip_connection_count}",
211
+ client_id=client_id,
212
+ operation="connection_validation",
213
+ additional_data={"client_ip": client_ip},
214
+ )
215
+
216
+ # Block IP temporarily for repeated violations
217
+ self._blocked_ips[client_ip] = current_time + 300.0 # 5 minute block
218
+
219
+ raise ResourceExhaustedError(
220
+ f"Maximum connections per IP exceeded: {ip_connection_count}"
221
+ )
222
+
223
+ def register_connection(self, client_id: str, client_ip: str) -> None:
224
+ """
225
+ Register a new WebSocket connection.
226
+
227
+ Args:
228
+ client_id: Unique client identifier
229
+ client_ip: Client IP address
230
+ """
231
+
232
+ with self._lock:
233
+ metrics = ConnectionMetrics(client_id=client_id)
234
+ self._connections[client_id] = metrics
235
+ self._ip_connections[client_ip].add(client_id)
236
+
237
+ self.security_logger.log_security_event(
238
+ event_type=SecurityEventType.CONNECTION_ESTABLISHED,
239
+ level=SecurityEventLevel.INFO,
240
+ message=f"WebSocket connection registered: {client_id}",
241
+ client_id=client_id,
242
+ operation="connection_registration",
243
+ additional_data={"client_ip": client_ip},
244
+ )
245
+
246
+ def unregister_connection(self, client_id: str, client_ip: str) -> None:
247
+ """
248
+ Unregister a WebSocket connection.
249
+
250
+ Args:
251
+ client_id: Unique client identifier
252
+ client_ip: Client IP address
253
+ """
254
+
255
+ with self._lock:
256
+ if client_id in self._connections:
257
+ metrics = self._connections[client_id]
258
+ del self._connections[client_id]
259
+
260
+ self.security_logger.log_security_event(
261
+ event_type=SecurityEventType.CONNECTION_CLOSED,
262
+ level=SecurityEventLevel.INFO,
263
+ message=f"WebSocket connection unregistered: {client_id}",
264
+ client_id=client_id,
265
+ operation="connection_unregistration",
266
+ additional_data={
267
+ "client_ip": client_ip,
268
+ "connection_duration": metrics.connection_duration,
269
+ "message_count": metrics.message_count,
270
+ "bytes_transferred": metrics.bytes_sent
271
+ + metrics.bytes_received,
272
+ },
273
+ )
274
+
275
+ # Remove from IP tracking
276
+ if client_ip in self._ip_connections:
277
+ self._ip_connections[client_ip].discard(client_id)
278
+ if not self._ip_connections[client_ip]:
279
+ del self._ip_connections[client_ip]
280
+
281
+ # Clear message history
282
+ if client_id in self._message_history:
283
+ del self._message_history[client_id]
284
+
285
+ def validate_message(self, client_id: str, message_size: int) -> None:
286
+ """
287
+ Validate if a message can be processed.
288
+
289
+ Args:
290
+ client_id: Client identifier
291
+ message_size: Size of the message in bytes
292
+
293
+ Raises:
294
+ ResourceExhaustedError: If message limits exceeded
295
+ """
296
+
297
+ with self._lock:
298
+ self._check_message_size(client_id, message_size)
299
+ metrics = self._get_connection_metrics(client_id)
300
+ self._check_message_count(client_id, metrics)
301
+ self._check_message_rate(client_id)
302
+
303
+ def _check_message_size(self, client_id: str, message_size: int) -> None:
304
+ """Check if message size exceeds limits."""
305
+ if message_size > self.limits.max_message_size:
306
+ self.security_logger.log_security_event(
307
+ event_type=SecurityEventType.RATE_LIMIT_EXCEEDED,
308
+ level=SecurityEventLevel.HIGH,
309
+ message=f"Message size limit exceeded: {message_size} bytes",
310
+ client_id=client_id,
311
+ operation="message_validation",
312
+ )
313
+ raise ResourceExhaustedError(f"Message too large: {message_size} bytes")
314
+
315
+ def _get_connection_metrics(self, client_id: str) -> ConnectionMetrics:
316
+ """Get connection metrics, raising error if connection doesn't exist."""
317
+ if client_id not in self._connections:
318
+ raise ResourceExhaustedError(f"Connection not registered: {client_id}")
319
+ return self._connections[client_id]
320
+
321
+ def _check_message_count(self, client_id: str, metrics: ConnectionMetrics) -> None:
322
+ """Check if total message count per connection exceeds limits."""
323
+ if metrics.message_count >= self.limits.max_messages_per_connection:
324
+ self.security_logger.log_security_event(
325
+ event_type=SecurityEventType.RATE_LIMIT_EXCEEDED,
326
+ level=SecurityEventLevel.WARNING,
327
+ message=f"Message count limit exceeded: {metrics.message_count}",
328
+ client_id=client_id,
329
+ operation="message_validation",
330
+ )
331
+ raise ResourceExhaustedError(
332
+ f"Message count limit exceeded: {metrics.message_count}"
333
+ )
334
+
335
+ def _check_message_rate(self, client_id: str) -> None:
336
+ """Check if message rate exceeds per-minute limits."""
337
+ current_time = time.time()
338
+ message_times = self._message_history[client_id]
339
+
340
+ # Remove old messages (older than 1 minute)
341
+ while message_times and current_time - message_times[0] > 60.0:
342
+ message_times.popleft()
343
+
344
+ if len(message_times) >= self.limits.max_messages_per_minute:
345
+ self.security_logger.log_security_event(
346
+ event_type=SecurityEventType.RATE_LIMIT_EXCEEDED,
347
+ level=SecurityEventLevel.WARNING,
348
+ message=f"Message rate limit exceeded: {len(message_times)} messages/min",
349
+ client_id=client_id,
350
+ operation="message_validation",
351
+ )
352
+ raise ResourceExhaustedError(
353
+ f"Message rate limit exceeded: {len(message_times)} messages/min"
354
+ )
355
+
356
+ def track_message(
357
+ self, client_id: str, message_size: int, is_sent: bool = True
358
+ ) -> None:
359
+ """
360
+ Track a processed message.
361
+
362
+ Args:
363
+ client_id: Client identifier
364
+ message_size: Size of the message in bytes
365
+ is_sent: True if message was sent, False if received
366
+ """
367
+
368
+ with self._lock:
369
+ current_time = time.time()
370
+
371
+ if client_id in self._connections:
372
+ metrics = self._connections[client_id]
373
+ metrics.message_count += 1
374
+ metrics.last_activity = current_time
375
+
376
+ if is_sent:
377
+ metrics.bytes_sent += message_size
378
+ else:
379
+ metrics.bytes_received += message_size
380
+
381
+ # Track message timing for rate limiting
382
+ self._message_history[client_id].append(current_time)
383
+
384
+ async def check_connection_limits(self, client_id: str) -> None:
385
+ """
386
+ Check if connection has exceeded limits.
387
+
388
+ Args:
389
+ client_id: Client identifier
390
+
391
+ Raises:
392
+ ResourceExhaustedError: If connection limits exceeded
393
+ """
394
+
395
+ with self._lock:
396
+ if client_id not in self._connections:
397
+ return
398
+
399
+ metrics = self._connections[client_id]
400
+ time.time()
401
+
402
+ # Check connection duration
403
+ if metrics.connection_duration > self.limits.max_connection_duration:
404
+ self.security_logger.log_security_event(
405
+ event_type=SecurityEventType.CONNECTION_TIMEOUT,
406
+ level=SecurityEventLevel.WARNING,
407
+ message=f"Connection duration exceeded: {metrics.connection_duration:.1f}s",
408
+ client_id=client_id,
409
+ operation="connection_limit_check",
410
+ )
411
+ raise ResourceExhaustedError(
412
+ f"Connection duration exceeded: {metrics.connection_duration:.1f}s"
413
+ )
414
+
415
+ # Check idle time
416
+ if metrics.idle_time > self.limits.max_idle_time:
417
+ self.security_logger.log_security_event(
418
+ event_type=SecurityEventType.CONNECTION_IDLE,
419
+ level=SecurityEventLevel.INFO,
420
+ message=f"Connection idle timeout: {metrics.idle_time:.1f}s",
421
+ client_id=client_id,
422
+ operation="connection_limit_check",
423
+ )
424
+ raise ResourceExhaustedError(
425
+ f"Connection idle timeout: {metrics.idle_time:.1f}s"
426
+ )
427
+
428
+ async def _cleanup_loop(self) -> None:
429
+ """Background cleanup loop for expired connections and resources."""
430
+
431
+ while not self._shutdown_event.is_set():
432
+ try:
433
+ await asyncio.wait_for(
434
+ self._shutdown_event.wait(),
435
+ timeout=30.0, # Cleanup every 30 seconds
436
+ )
437
+ break # Shutdown requested
438
+
439
+ except TimeoutError:
440
+ # Perform cleanup
441
+ await self._perform_cleanup()
442
+
443
+ self.security_logger.log_security_event(
444
+ event_type=SecurityEventType.SERVICE_CLEANUP,
445
+ level=SecurityEventLevel.INFO,
446
+ message="WebSocket resource limiter cleanup loop ended",
447
+ operation="cleanup_loop",
448
+ )
449
+
450
+ async def _perform_cleanup(self) -> None:
451
+ """Perform resource cleanup."""
452
+ current_time = time.time()
453
+
454
+ with self._lock:
455
+ cleanup_count = self._cleanup_expired_connections(current_time)
456
+ self._cleanup_empty_ip_entries()
457
+ self._cleanup_expired_ip_blocks(current_time)
458
+
459
+ self._log_cleanup_results(cleanup_count)
460
+
461
+ def _cleanup_expired_connections(self, current_time: float) -> int:
462
+ """Clean up expired connections and return count of cleaned connections."""
463
+ expired_connections = self._find_expired_connections()
464
+ cleanup_count = 0
465
+
466
+ for client_id in expired_connections:
467
+ if self._remove_expired_connection(client_id):
468
+ cleanup_count += 1
469
+
470
+ return cleanup_count
471
+
472
+ def _find_expired_connections(self) -> list[str]:
473
+ """Find connections that have exceeded duration or idle time limits."""
474
+ return [
475
+ client_id
476
+ for client_id, metrics in self._connections.items()
477
+ if (
478
+ metrics.connection_duration > self.limits.max_connection_duration
479
+ or metrics.idle_time > self.limits.max_idle_time
480
+ )
481
+ ]
482
+
483
+ def _remove_expired_connection(self, client_id: str) -> bool:
484
+ """Remove a specific expired connection and its associated data."""
485
+ if client_id not in self._connections:
486
+ return False
487
+
488
+ # Remove connection metrics
489
+ del self._connections[client_id]
490
+
491
+ # Clean up IP tracking
492
+ for client_set in self._ip_connections.values():
493
+ client_set.discard(client_id)
494
+
495
+ # Clean up message history
496
+ if client_id in self._message_history:
497
+ del self._message_history[client_id]
498
+
499
+ return True
500
+
501
+ def _cleanup_empty_ip_entries(self) -> None:
502
+ """Remove IP entries that no longer have any connections."""
503
+ empty_ips = [ip for ip, clients in self._ip_connections.items() if not clients]
504
+ for ip in empty_ips:
505
+ del self._ip_connections[ip]
506
+
507
+ def _cleanup_expired_ip_blocks(self, current_time: float) -> None:
508
+ """Remove IP blocks that have expired."""
509
+ expired_blocks = [
510
+ ip
511
+ for ip, block_until in self._blocked_ips.items()
512
+ if current_time >= block_until
513
+ ]
514
+ for ip in expired_blocks:
515
+ del self._blocked_ips[ip]
516
+
517
+ def _log_cleanup_results(self, cleanup_count: int) -> None:
518
+ """Log cleanup results if any connections were cleaned up."""
519
+ if cleanup_count > 0:
520
+ self.security_logger.log_security_event(
521
+ event_type=SecurityEventType.RESOURCE_CLEANUP,
522
+ level=SecurityEventLevel.INFO,
523
+ message=f"Cleaned up {cleanup_count} expired connections",
524
+ operation="resource_cleanup",
525
+ )
526
+
527
+ def get_resource_status(self) -> dict[str, t.Any]:
528
+ """Get current resource usage status."""
529
+
530
+ with self._lock:
531
+ return {
532
+ "connections": {
533
+ "total": len(self._connections),
534
+ "limit": self.limits.max_connections,
535
+ "utilization": len(self._connections) / self.limits.max_connections,
536
+ },
537
+ "ip_distribution": {
538
+ ip: len(clients) for ip, clients in self._ip_connections.items()
539
+ },
540
+ "blocked_ips": len(self._blocked_ips),
541
+ "memory_usage_mb": self._memory_usage,
542
+ "memory_limit_mb": self.limits.max_memory_usage_mb,
543
+ "limits": {
544
+ "max_connections": self.limits.max_connections,
545
+ "max_connections_per_ip": self.limits.max_connections_per_ip,
546
+ "max_message_size": self.limits.max_message_size,
547
+ "max_messages_per_minute": self.limits.max_messages_per_minute,
548
+ "max_connection_duration": self.limits.max_connection_duration,
549
+ "max_idle_time": self.limits.max_idle_time,
550
+ },
551
+ }
552
+
553
+ def get_connection_metrics(self, client_id: str) -> ConnectionMetrics | None:
554
+ """Get metrics for a specific connection."""
555
+
556
+ with self._lock:
557
+ return self._connections.get(client_id)
558
+
559
+
560
+ # Global singleton instance
561
+ _resource_limiter: WebSocketResourceLimiter | None = None
562
+
563
+
564
+ def get_websocket_resource_limiter(
565
+ limits: ResourceLimits | None = None,
566
+ ) -> WebSocketResourceLimiter:
567
+ """Get the global WebSocket resource limiter instance."""
568
+
569
+ global _resource_limiter
570
+ if _resource_limiter is None:
571
+ _resource_limiter = WebSocketResourceLimiter(limits)
572
+ return _resource_limiter
@@ -1,14 +1,64 @@
1
1
  from pathlib import Path
2
2
 
3
+ from ..errors import ErrorCode, ExecutionError
4
+ from ..services.input_validator import validate_and_sanitize_string
5
+
3
6
  SLASH_COMMANDS_DIR = Path(__file__).parent
4
7
 
5
8
 
6
9
  def get_slash_command_path(command_name: str) -> Path:
7
- return SLASH_COMMANDS_DIR / f"{command_name}.md"
10
+ """Get path to slash command file with validation."""
11
+ try:
12
+ # Validate command name to prevent directory traversal
13
+ sanitized_name = validate_and_sanitize_string(
14
+ command_name, max_length=50, strict_alphanumeric=True
15
+ )
16
+
17
+ command_path = SLASH_COMMANDS_DIR / f"{sanitized_name}.md"
18
+
19
+ # Ensure the path stays within the slash commands directory
20
+ if not str(command_path.resolve()).startswith(
21
+ str(SLASH_COMMANDS_DIR.resolve())
22
+ ):
23
+ raise ExecutionError(
24
+ message=f"Command path outside allowed directory: {command_name}",
25
+ error_code=ErrorCode.VALIDATION_ERROR,
26
+ )
27
+
28
+ return command_path
29
+
30
+ except Exception as e:
31
+ if isinstance(e, ExecutionError):
32
+ raise
33
+ raise ExecutionError(
34
+ message=f"Invalid command name: {command_name}",
35
+ error_code=ErrorCode.VALIDATION_ERROR,
36
+ ) from e
8
37
 
9
38
 
10
39
  def list_available_commands() -> list[str]:
11
- return [f.stem for f in SLASH_COMMANDS_DIR.glob("*.md")]
40
+ """List available slash commands with validation."""
41
+ try:
42
+ commands = []
43
+ for file_path in SLASH_COMMANDS_DIR.glob("*.md"):
44
+ command_name = file_path.stem
45
+ # Validate each command name
46
+ try:
47
+ validate_and_sanitize_string(
48
+ command_name, max_length=50, strict_alphanumeric=True
49
+ )
50
+ commands.append(command_name)
51
+ except ExecutionError:
52
+ # Skip invalid command names
53
+ continue
54
+
55
+ return sorted(commands)
56
+
57
+ except Exception as e:
58
+ raise ExecutionError(
59
+ message="Failed to list available commands",
60
+ error_code=ErrorCode.FILE_READ_ERROR,
61
+ ) from e
12
62
 
13
63
 
14
64
  __all__ = ["SLASH_COMMANDS_DIR", "get_slash_command_path", "list_available_commands"]
File without changes