crackerjack 0.31.9__py3-none-any.whl → 0.31.12__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.
- crackerjack/CLAUDE.md +288 -705
- crackerjack/__main__.py +22 -8
- crackerjack/agents/__init__.py +0 -3
- crackerjack/agents/architect_agent.py +0 -43
- crackerjack/agents/base.py +1 -9
- crackerjack/agents/coordinator.py +2 -148
- crackerjack/agents/documentation_agent.py +109 -81
- crackerjack/agents/dry_agent.py +122 -97
- crackerjack/agents/formatting_agent.py +3 -16
- crackerjack/agents/import_optimization_agent.py +1174 -130
- crackerjack/agents/performance_agent.py +956 -188
- crackerjack/agents/performance_helpers.py +229 -0
- crackerjack/agents/proactive_agent.py +1 -48
- crackerjack/agents/refactoring_agent.py +516 -246
- crackerjack/agents/refactoring_helpers.py +282 -0
- crackerjack/agents/security_agent.py +393 -90
- crackerjack/agents/test_creation_agent.py +1776 -120
- crackerjack/agents/test_specialist_agent.py +59 -15
- crackerjack/agents/tracker.py +0 -102
- crackerjack/api.py +145 -37
- crackerjack/cli/handlers.py +48 -30
- crackerjack/cli/interactive.py +11 -11
- crackerjack/cli/options.py +66 -4
- crackerjack/code_cleaner.py +808 -148
- crackerjack/config/global_lock_config.py +110 -0
- crackerjack/config/hooks.py +43 -64
- crackerjack/core/async_workflow_orchestrator.py +247 -97
- crackerjack/core/autofix_coordinator.py +192 -109
- crackerjack/core/enhanced_container.py +46 -63
- crackerjack/core/file_lifecycle.py +549 -0
- crackerjack/core/performance.py +9 -8
- crackerjack/core/performance_monitor.py +395 -0
- crackerjack/core/phase_coordinator.py +282 -95
- crackerjack/core/proactive_workflow.py +9 -58
- crackerjack/core/resource_manager.py +501 -0
- crackerjack/core/service_watchdog.py +490 -0
- crackerjack/core/session_coordinator.py +4 -8
- crackerjack/core/timeout_manager.py +504 -0
- crackerjack/core/websocket_lifecycle.py +475 -0
- crackerjack/core/workflow_orchestrator.py +355 -204
- crackerjack/dynamic_config.py +47 -6
- crackerjack/errors.py +3 -4
- crackerjack/executors/async_hook_executor.py +63 -13
- crackerjack/executors/cached_hook_executor.py +14 -14
- crackerjack/executors/hook_executor.py +100 -37
- crackerjack/executors/hook_lock_manager.py +856 -0
- crackerjack/executors/individual_hook_executor.py +120 -86
- crackerjack/intelligence/__init__.py +0 -7
- crackerjack/intelligence/adaptive_learning.py +13 -86
- crackerjack/intelligence/agent_orchestrator.py +15 -78
- crackerjack/intelligence/agent_registry.py +12 -59
- crackerjack/intelligence/agent_selector.py +31 -92
- crackerjack/intelligence/integration.py +1 -41
- crackerjack/interactive.py +9 -9
- crackerjack/managers/async_hook_manager.py +25 -8
- crackerjack/managers/hook_manager.py +9 -9
- crackerjack/managers/publish_manager.py +57 -59
- crackerjack/managers/test_command_builder.py +6 -36
- crackerjack/managers/test_executor.py +9 -61
- crackerjack/managers/test_manager.py +52 -62
- crackerjack/managers/test_manager_backup.py +77 -127
- crackerjack/managers/test_progress.py +4 -23
- crackerjack/mcp/cache.py +5 -12
- crackerjack/mcp/client_runner.py +10 -10
- crackerjack/mcp/context.py +64 -6
- crackerjack/mcp/dashboard.py +14 -11
- crackerjack/mcp/enhanced_progress_monitor.py +55 -55
- crackerjack/mcp/file_monitor.py +72 -42
- crackerjack/mcp/progress_components.py +103 -84
- crackerjack/mcp/progress_monitor.py +122 -49
- crackerjack/mcp/rate_limiter.py +12 -12
- crackerjack/mcp/server_core.py +16 -22
- crackerjack/mcp/service_watchdog.py +26 -26
- crackerjack/mcp/state.py +15 -0
- crackerjack/mcp/tools/core_tools.py +95 -39
- crackerjack/mcp/tools/error_analyzer.py +6 -32
- crackerjack/mcp/tools/execution_tools.py +1 -56
- crackerjack/mcp/tools/execution_tools_backup.py +35 -131
- crackerjack/mcp/tools/intelligence_tool_registry.py +0 -36
- crackerjack/mcp/tools/intelligence_tools.py +2 -55
- crackerjack/mcp/tools/monitoring_tools.py +308 -145
- crackerjack/mcp/tools/proactive_tools.py +12 -42
- crackerjack/mcp/tools/progress_tools.py +23 -15
- crackerjack/mcp/tools/utility_tools.py +3 -40
- crackerjack/mcp/tools/workflow_executor.py +40 -60
- crackerjack/mcp/websocket/app.py +0 -3
- crackerjack/mcp/websocket/endpoints.py +206 -268
- crackerjack/mcp/websocket/jobs.py +213 -66
- crackerjack/mcp/websocket/server.py +84 -6
- crackerjack/mcp/websocket/websocket_handler.py +137 -29
- crackerjack/models/config_adapter.py +3 -16
- crackerjack/models/protocols.py +162 -3
- crackerjack/models/resource_protocols.py +454 -0
- crackerjack/models/task.py +3 -3
- crackerjack/monitoring/__init__.py +0 -0
- crackerjack/monitoring/ai_agent_watchdog.py +25 -71
- crackerjack/monitoring/regression_prevention.py +28 -87
- crackerjack/orchestration/advanced_orchestrator.py +44 -78
- crackerjack/orchestration/coverage_improvement.py +10 -60
- crackerjack/orchestration/execution_strategies.py +16 -16
- crackerjack/orchestration/test_progress_streamer.py +61 -53
- crackerjack/plugins/base.py +1 -1
- crackerjack/plugins/managers.py +22 -20
- crackerjack/py313.py +65 -21
- crackerjack/services/backup_service.py +467 -0
- crackerjack/services/bounded_status_operations.py +627 -0
- crackerjack/services/cache.py +7 -9
- crackerjack/services/config.py +35 -52
- crackerjack/services/config_integrity.py +5 -16
- crackerjack/services/config_merge.py +542 -0
- crackerjack/services/contextual_ai_assistant.py +17 -19
- crackerjack/services/coverage_ratchet.py +51 -76
- crackerjack/services/debug.py +25 -39
- crackerjack/services/dependency_monitor.py +52 -50
- crackerjack/services/enhanced_filesystem.py +14 -11
- crackerjack/services/file_hasher.py +1 -1
- crackerjack/services/filesystem.py +1 -12
- crackerjack/services/git.py +78 -44
- crackerjack/services/health_metrics.py +31 -27
- crackerjack/services/initialization.py +281 -433
- crackerjack/services/input_validator.py +760 -0
- crackerjack/services/log_manager.py +16 -16
- crackerjack/services/logging.py +7 -6
- crackerjack/services/metrics.py +43 -43
- crackerjack/services/pattern_cache.py +2 -31
- crackerjack/services/pattern_detector.py +26 -63
- crackerjack/services/performance_benchmarks.py +20 -45
- crackerjack/services/regex_patterns.py +2887 -0
- crackerjack/services/regex_utils.py +537 -0
- crackerjack/services/secure_path_utils.py +683 -0
- crackerjack/services/secure_status_formatter.py +534 -0
- crackerjack/services/secure_subprocess.py +605 -0
- crackerjack/services/security.py +47 -10
- crackerjack/services/security_logger.py +492 -0
- crackerjack/services/server_manager.py +109 -50
- crackerjack/services/smart_scheduling.py +8 -25
- crackerjack/services/status_authentication.py +603 -0
- crackerjack/services/status_security_manager.py +442 -0
- crackerjack/services/thread_safe_status_collector.py +546 -0
- crackerjack/services/tool_version_service.py +1 -23
- crackerjack/services/unified_config.py +36 -58
- crackerjack/services/validation_rate_limiter.py +269 -0
- crackerjack/services/version_checker.py +9 -40
- crackerjack/services/websocket_resource_limiter.py +572 -0
- crackerjack/slash_commands/__init__.py +52 -2
- crackerjack/tools/__init__.py +0 -0
- crackerjack/tools/validate_input_validator_patterns.py +262 -0
- crackerjack/tools/validate_regex_patterns.py +198 -0
- {crackerjack-0.31.9.dist-info → crackerjack-0.31.12.dist-info}/METADATA +197 -12
- crackerjack-0.31.12.dist-info/RECORD +178 -0
- crackerjack/cli/facade.py +0 -104
- crackerjack-0.31.9.dist-info/RECORD +0 -149
- {crackerjack-0.31.9.dist-info → crackerjack-0.31.12.dist-info}/WHEEL +0 -0
- {crackerjack-0.31.9.dist-info → crackerjack-0.31.12.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.31.9.dist-info → crackerjack-0.31.12.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
|
-
|
|
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
|
-
|
|
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
|