qyro 2.0.0__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.
- qyro/__init__.py +17 -0
- qyro/adapters/__init__.py +4 -0
- qyro/adapters/language_adapters/__init__.py +4 -0
- qyro/adapters/language_adapters/c/__init__.py +4 -0
- qyro/adapters/language_adapters/python/__init__.py +4 -0
- qyro/adapters/language_adapters/python/python_adapter.py +584 -0
- qyro/cli/__init__.py +8 -0
- qyro/cli/__main__.py +5 -0
- qyro/cli/cli.py +392 -0
- qyro/cli/interactive.py +297 -0
- qyro/common/__init__.py +37 -0
- qyro/common/animation.py +82 -0
- qyro/common/builder.py +434 -0
- qyro/common/compiler.py +895 -0
- qyro/common/config.py +93 -0
- qyro/common/constants.py +99 -0
- qyro/common/errors.py +176 -0
- qyro/common/frontend.py +74 -0
- qyro/common/health.py +358 -0
- qyro/common/kafka_manager.py +192 -0
- qyro/common/logging.py +149 -0
- qyro/common/memory.py +147 -0
- qyro/common/metrics.py +301 -0
- qyro/common/monitoring.py +468 -0
- qyro/common/parser.py +91 -0
- qyro/common/platform.py +609 -0
- qyro/common/redis_memory.py +1108 -0
- qyro/common/rpc.py +287 -0
- qyro/common/sandbox.py +191 -0
- qyro/common/schema_loader.py +33 -0
- qyro/common/secure_sandbox.py +490 -0
- qyro/common/toolchain_validator.py +617 -0
- qyro/common/type_generator.py +176 -0
- qyro/common/validation.py +401 -0
- qyro/common/validator.py +204 -0
- qyro/gateway/__init__.py +8 -0
- qyro/gateway/gateway.py +303 -0
- qyro/orchestrator/__init__.py +8 -0
- qyro/orchestrator/orchestrator.py +1223 -0
- qyro-2.0.0.dist-info/METADATA +244 -0
- qyro-2.0.0.dist-info/RECORD +45 -0
- qyro-2.0.0.dist-info/WHEEL +5 -0
- qyro-2.0.0.dist-info/entry_points.txt +2 -0
- qyro-2.0.0.dist-info/licenses/LICENSE +21 -0
- qyro-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import ast
|
|
3
|
+
|
|
4
|
+
class QyroTypeGenerator:
|
|
5
|
+
def __init__(self, schema_json):
|
|
6
|
+
self.schema = json.loads(schema_json) if isinstance(schema_json, str) else schema_json
|
|
7
|
+
|
|
8
|
+
def _infer_type_from_value(self, value):
|
|
9
|
+
"""Infer the appropriate type for a given value."""
|
|
10
|
+
if isinstance(value, list):
|
|
11
|
+
if len(value) > 0:
|
|
12
|
+
element_type = self._infer_type_from_value(value[0])
|
|
13
|
+
return f"list[{element_type}]"
|
|
14
|
+
else:
|
|
15
|
+
return "list[any]"
|
|
16
|
+
elif isinstance(value, dict):
|
|
17
|
+
return "dict"
|
|
18
|
+
elif isinstance(value, int):
|
|
19
|
+
return "int"
|
|
20
|
+
elif isinstance(value, float):
|
|
21
|
+
return "float"
|
|
22
|
+
elif isinstance(value, str):
|
|
23
|
+
return "string"
|
|
24
|
+
elif isinstance(value, bool):
|
|
25
|
+
return "bool"
|
|
26
|
+
else:
|
|
27
|
+
return "any"
|
|
28
|
+
|
|
29
|
+
def _get_c_type(self, value):
|
|
30
|
+
"""Get appropriate C type for a value."""
|
|
31
|
+
if isinstance(value, list):
|
|
32
|
+
if len(value) > 0:
|
|
33
|
+
element_type = self._get_c_type(value[0])
|
|
34
|
+
# For C, we'll use void* for generic list and handle serialization separately
|
|
35
|
+
return "void*" # Will be serialized as JSON string
|
|
36
|
+
else:
|
|
37
|
+
return "void*"
|
|
38
|
+
elif isinstance(value, dict):
|
|
39
|
+
return "void*" # Will be serialized as JSON string
|
|
40
|
+
elif isinstance(value, int):
|
|
41
|
+
if -2147483648 <= value <= 2147483647:
|
|
42
|
+
return "int"
|
|
43
|
+
else:
|
|
44
|
+
return "long long"
|
|
45
|
+
elif isinstance(value, float):
|
|
46
|
+
return "double"
|
|
47
|
+
elif isinstance(value, str):
|
|
48
|
+
return "char*" # Dynamic string allocation instead of fixed size
|
|
49
|
+
elif isinstance(value, bool):
|
|
50
|
+
return "int" # C doesn't have bool, use int
|
|
51
|
+
else:
|
|
52
|
+
return "void*"
|
|
53
|
+
|
|
54
|
+
def _get_rust_type(self, value):
|
|
55
|
+
"""Get appropriate Rust type for a value."""
|
|
56
|
+
if isinstance(value, list):
|
|
57
|
+
if len(value) > 0:
|
|
58
|
+
element_type = self._get_rust_type(value[0])
|
|
59
|
+
return f"Vec<{element_type}>"
|
|
60
|
+
else:
|
|
61
|
+
return "Vec<any>"
|
|
62
|
+
elif isinstance(value, dict):
|
|
63
|
+
return "std::collections::HashMap<String, serde_json::Value>"
|
|
64
|
+
elif isinstance(value, int):
|
|
65
|
+
if -2147483648 <= value <= 2147483647:
|
|
66
|
+
return "i32"
|
|
67
|
+
else:
|
|
68
|
+
return "i64"
|
|
69
|
+
elif isinstance(value, float):
|
|
70
|
+
return "f64"
|
|
71
|
+
elif isinstance(value, str):
|
|
72
|
+
return "String"
|
|
73
|
+
elif isinstance(value, bool):
|
|
74
|
+
return "bool"
|
|
75
|
+
else:
|
|
76
|
+
return "serde_json::Value"
|
|
77
|
+
|
|
78
|
+
def _get_java_type(self, value):
|
|
79
|
+
"""Get appropriate Java type for a value."""
|
|
80
|
+
if isinstance(value, list):
|
|
81
|
+
if len(value) > 0:
|
|
82
|
+
element_type = self._get_java_type(value[0])
|
|
83
|
+
return f"java.util.List<{element_type}>"
|
|
84
|
+
else:
|
|
85
|
+
return "java.util.List<Object>"
|
|
86
|
+
elif isinstance(value, dict):
|
|
87
|
+
return "java.util.Map<String, Object>"
|
|
88
|
+
elif isinstance(value, int):
|
|
89
|
+
if -2147483648 <= value <= 2147483647:
|
|
90
|
+
return "int"
|
|
91
|
+
else:
|
|
92
|
+
return "long"
|
|
93
|
+
elif isinstance(value, float):
|
|
94
|
+
return "double"
|
|
95
|
+
elif isinstance(value, str):
|
|
96
|
+
return "String"
|
|
97
|
+
elif isinstance(value, bool):
|
|
98
|
+
return "boolean"
|
|
99
|
+
else:
|
|
100
|
+
return "Object"
|
|
101
|
+
|
|
102
|
+
def _get_ts_type(self, value):
|
|
103
|
+
"""Get appropriate TypeScript type for a value."""
|
|
104
|
+
if isinstance(value, list):
|
|
105
|
+
if len(value) > 0:
|
|
106
|
+
element_type = self._get_ts_type(value[0])
|
|
107
|
+
return f"{element_type}[]"
|
|
108
|
+
else:
|
|
109
|
+
return "any[]"
|
|
110
|
+
elif isinstance(value, dict):
|
|
111
|
+
return "{ [key: string]: any }"
|
|
112
|
+
elif isinstance(value, int) or isinstance(value, float):
|
|
113
|
+
return "number"
|
|
114
|
+
elif isinstance(value, str):
|
|
115
|
+
return "string"
|
|
116
|
+
elif isinstance(value, bool):
|
|
117
|
+
return "boolean"
|
|
118
|
+
else:
|
|
119
|
+
return "any"
|
|
120
|
+
|
|
121
|
+
def generate_c_structs(self):
|
|
122
|
+
code = "#include <stdint.h>\n#include <stdlib.h>\n#include <string.h>\n\n"
|
|
123
|
+
code += "// Note: Complex types (lists, dicts) are stored as JSON strings and require serialization/deserialization\n"
|
|
124
|
+
code += "typedef struct {\n"
|
|
125
|
+
for key, value in self.schema.items():
|
|
126
|
+
c_type = self._get_c_type(value)
|
|
127
|
+
if c_type == "char*":
|
|
128
|
+
# For strings, we'll use char* instead of fixed-size arrays to avoid buffer overflows
|
|
129
|
+
code += f" {c_type} {key}; // Dynamically allocated string\n"
|
|
130
|
+
else:
|
|
131
|
+
code += f" {c_type} {key};\n"
|
|
132
|
+
code += "} GlobalState;\n\n"
|
|
133
|
+
# Add helper functions for managing dynamic strings
|
|
134
|
+
code += "// Helper functions for dynamic memory management\n"
|
|
135
|
+
code += "void init_GlobalState(GlobalState* state) {\n"
|
|
136
|
+
for key, value in self.schema.items():
|
|
137
|
+
c_type = self._get_c_type(value)
|
|
138
|
+
if c_type == "char*":
|
|
139
|
+
code += f" state->{key} = NULL;\n"
|
|
140
|
+
elif "void*" in c_type: # For lists and dicts
|
|
141
|
+
code += f" state->{key} = NULL;\n"
|
|
142
|
+
code += "}\n\n"
|
|
143
|
+
code += "void free_GlobalState(GlobalState* state) {\n"
|
|
144
|
+
for key, value in self.schema.items():
|
|
145
|
+
c_type = self._get_c_type(value)
|
|
146
|
+
if c_type == "char*":
|
|
147
|
+
code += f" if (state->{key}) free(state->{key});\n"
|
|
148
|
+
elif "void*" in c_type: # For lists and dicts
|
|
149
|
+
code += f" if (state->{key}) free(state->{key});\n"
|
|
150
|
+
code += "}\n"
|
|
151
|
+
return code
|
|
152
|
+
|
|
153
|
+
def generate_rust_structs(self):
|
|
154
|
+
code = "use serde::{Serialize, Deserialize};\nuse std::collections::HashMap;\n\n"
|
|
155
|
+
code += "#[derive(Serialize, Deserialize, Debug, Clone)]\npub struct GlobalState {\n"
|
|
156
|
+
for key, value in self.schema.items():
|
|
157
|
+
rust_type = self._get_rust_type(value)
|
|
158
|
+
code += f" pub {key}: {rust_type},\n"
|
|
159
|
+
code += "}\n"
|
|
160
|
+
return code
|
|
161
|
+
|
|
162
|
+
def generate_java_class(self):
|
|
163
|
+
code = "package nexus;\n\nimport java.util.*;\n\npublic class GlobalState {\n"
|
|
164
|
+
for key, value in self.schema.items():
|
|
165
|
+
java_type = self._get_java_type(value)
|
|
166
|
+
code += f" public {java_type} {key};\n"
|
|
167
|
+
code += "}\n"
|
|
168
|
+
return code
|
|
169
|
+
|
|
170
|
+
def generate_ts_interface(self):
|
|
171
|
+
code = "export interface GlobalState {\n"
|
|
172
|
+
for key, value in self.schema.items():
|
|
173
|
+
ts_type = self._get_ts_type(value)
|
|
174
|
+
code += f" {key}: {ts_type};\n"
|
|
175
|
+
code += "}\n"
|
|
176
|
+
return code
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Nexus Input Validation and Sanitization
|
|
3
|
+
Production-grade validation and sanitization for secure data handling.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from typing import Any, Dict, List, Optional, Union, Tuple
|
|
9
|
+
from .errors import NexusError, ErrorCode, JSONError, Result
|
|
10
|
+
import html
|
|
11
|
+
import urllib.parse
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ValidationError(NexusError):
|
|
15
|
+
"""Specific exception for validation errors."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class InputValidator:
|
|
20
|
+
"""
|
|
21
|
+
Production-grade input validation system with multiple layers of security.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
# Dangerous patterns that should be blocked
|
|
25
|
+
DANGEROUS_PATTERNS = [
|
|
26
|
+
# SQL injection patterns
|
|
27
|
+
r"(?i)(union\s+select|drop\s+\w+|delete\s+from|insert\s+into|update\s+\w+\s+set)",
|
|
28
|
+
# Command injection patterns
|
|
29
|
+
r"(?i)(exec|system|popen|subprocess|os\.)",
|
|
30
|
+
# Path traversal
|
|
31
|
+
r"(\.\.\/|\.\.\\|%2e%2e%2f|%2e%2e%5c)",
|
|
32
|
+
# JavaScript injection
|
|
33
|
+
r"(?i)(<script|javascript:|on\w+\s*=)",
|
|
34
|
+
# File inclusion
|
|
35
|
+
r"(?i)(include|require|open\()",
|
|
36
|
+
# Network operations
|
|
37
|
+
r"(?i)(socket|connect|bind|listen|accept|urlopen|request)",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
# Safe patterns for different contexts
|
|
41
|
+
SAFE_PATTERNS = {
|
|
42
|
+
'identifier': r'^[a-zA-Z_][a-zA-Z0-9_]*$',
|
|
43
|
+
'email': r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$',
|
|
44
|
+
'url': r'^https?://[^\s/$.?#].[^\s]*$',
|
|
45
|
+
'phone': r'^\+?[\d\s\-\(\)]+$',
|
|
46
|
+
'alphanumeric': r'^[a-zA-Z0-9]+$',
|
|
47
|
+
'alpha': r'^[a-zA-Z]+$',
|
|
48
|
+
'numeric': r'^\d+$',
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def validate_pattern(cls, value: str, pattern_name: str) -> bool:
|
|
53
|
+
"""
|
|
54
|
+
Validate a string against a predefined safe pattern.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
value: String to validate
|
|
58
|
+
pattern_name: Name of the pattern to use
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
True if valid, False otherwise
|
|
62
|
+
"""
|
|
63
|
+
if pattern_name not in cls.SAFE_PATTERNS:
|
|
64
|
+
raise ValidationError(ErrorCode.JSON_PARSE_ERROR, f"Unknown pattern: {pattern_name}")
|
|
65
|
+
|
|
66
|
+
pattern = cls.SAFE_PATTERNS[pattern_name]
|
|
67
|
+
return bool(re.match(pattern, value))
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def validate_dangerous_content(cls, value: str) -> Tuple[bool, Optional[str]]:
|
|
71
|
+
"""
|
|
72
|
+
Check if a string contains dangerous patterns.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
value: String to check
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Tuple of (is_safe, reason_if_unsafe)
|
|
79
|
+
"""
|
|
80
|
+
for i, pattern in enumerate(cls.DANGEROUS_PATTERNS):
|
|
81
|
+
if re.search(pattern, value, re.IGNORECASE):
|
|
82
|
+
return False, f"Dangerous pattern #{i+1} detected"
|
|
83
|
+
return True, None
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def sanitize_string(cls, value: str, max_length: int = 10000, allow_html: bool = False) -> str:
|
|
87
|
+
"""
|
|
88
|
+
Sanitize a string input with multiple layers of protection.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
value: String to sanitize
|
|
92
|
+
max_length: Maximum allowed length
|
|
93
|
+
allow_html: Whether to allow HTML (will be escaped if False)
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Sanitized string
|
|
97
|
+
"""
|
|
98
|
+
if not isinstance(value, str):
|
|
99
|
+
value = str(value)
|
|
100
|
+
|
|
101
|
+
# Truncate to max length
|
|
102
|
+
value = value[:max_length]
|
|
103
|
+
|
|
104
|
+
# Remove null bytes
|
|
105
|
+
value = value.replace('\x00', '')
|
|
106
|
+
|
|
107
|
+
if not allow_html:
|
|
108
|
+
# Escape HTML entities
|
|
109
|
+
value = html.escape(value, quote=True)
|
|
110
|
+
|
|
111
|
+
return value
|
|
112
|
+
|
|
113
|
+
@classmethod
|
|
114
|
+
def sanitize_json(cls, data: Union[Dict, List, str], max_depth: int = 10) -> Union[Dict, List]:
|
|
115
|
+
"""
|
|
116
|
+
Recursively sanitize JSON data with depth protection.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
data: JSON data to sanitize
|
|
120
|
+
max_depth: Maximum recursion depth
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Sanitized JSON data
|
|
124
|
+
"""
|
|
125
|
+
return cls._sanitize_json_recursive(data, max_depth, 0)
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def _sanitize_json_recursive(cls, data: Any, max_depth: int, current_depth: int) -> Any:
|
|
129
|
+
"""Helper method for recursive sanitization."""
|
|
130
|
+
if current_depth > max_depth:
|
|
131
|
+
raise ValidationError(ErrorCode.JSON_PARSE_ERROR, f"JSON exceeds maximum depth of {max_depth}")
|
|
132
|
+
|
|
133
|
+
if isinstance(data, str):
|
|
134
|
+
return cls.sanitize_string(data)
|
|
135
|
+
elif isinstance(data, dict):
|
|
136
|
+
return {k: cls._sanitize_json_recursive(v, max_depth, current_depth + 1) for k, v in data.items()}
|
|
137
|
+
elif isinstance(data, list):
|
|
138
|
+
return [cls._sanitize_json_recursive(item, max_depth, current_depth + 1) for item in data]
|
|
139
|
+
else:
|
|
140
|
+
return data
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
def validate_json_structure(cls, data: Dict, schema: Dict, path: str = "root") -> List[str]:
|
|
144
|
+
"""
|
|
145
|
+
Validate JSON data against a schema with detailed error reporting.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
data: Data to validate
|
|
149
|
+
schema: Schema to validate against
|
|
150
|
+
path: Current path in the validation (for error reporting)
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
List of validation errors
|
|
154
|
+
"""
|
|
155
|
+
errors = []
|
|
156
|
+
|
|
157
|
+
for key, expected_type in schema.items():
|
|
158
|
+
full_path = f"{path}.{key}" if path != "root" else key
|
|
159
|
+
|
|
160
|
+
if key not in data:
|
|
161
|
+
if isinstance(expected_type, dict) and expected_type.get('_required', True):
|
|
162
|
+
errors.append(f"Missing required field: {full_path}")
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
actual_value = data[key]
|
|
166
|
+
actual_type = type(actual_value)
|
|
167
|
+
expected_python_type = cls._get_python_type(expected_type)
|
|
168
|
+
|
|
169
|
+
if actual_type != expected_python_type:
|
|
170
|
+
# Allow int for float fields
|
|
171
|
+
if not (expected_python_type == float and actual_type == int):
|
|
172
|
+
errors.append(f"Type mismatch at {full_path}: expected {expected_python_type.__name__}, got {actual_type.__name__}")
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
# Validate nested objects
|
|
176
|
+
if isinstance(expected_type, dict) and '_type' not in expected_type:
|
|
177
|
+
if isinstance(actual_value, dict):
|
|
178
|
+
errors.extend(cls.validate_json_structure(actual_value, expected_type, full_path))
|
|
179
|
+
else:
|
|
180
|
+
errors.append(f"Expected object at {full_path}, got {actual_type.__name__}")
|
|
181
|
+
|
|
182
|
+
return errors
|
|
183
|
+
|
|
184
|
+
@classmethod
|
|
185
|
+
def _get_python_type(cls, expected_type: Any) -> type:
|
|
186
|
+
"""Convert schema type to Python type."""
|
|
187
|
+
type_mapping = {
|
|
188
|
+
'string': str,
|
|
189
|
+
'number': (int, float),
|
|
190
|
+
'integer': int,
|
|
191
|
+
'boolean': bool,
|
|
192
|
+
'array': list,
|
|
193
|
+
'object': dict,
|
|
194
|
+
str: str,
|
|
195
|
+
int: int,
|
|
196
|
+
float: float,
|
|
197
|
+
bool: bool,
|
|
198
|
+
list: list,
|
|
199
|
+
dict: dict,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if isinstance(expected_type, str):
|
|
203
|
+
return type_mapping.get(expected_type, str)
|
|
204
|
+
elif isinstance(expected_type, type):
|
|
205
|
+
return expected_type
|
|
206
|
+
else:
|
|
207
|
+
return type(expected_type) if expected_type else type(None)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class SchemaValidator:
|
|
211
|
+
"""
|
|
212
|
+
Advanced schema validation with type safety and structure consistency.
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
def __init__(self, schema: Dict[str, Any]):
|
|
216
|
+
self.schema = schema
|
|
217
|
+
self.validator = InputValidator()
|
|
218
|
+
|
|
219
|
+
def validate(self, data: Dict[str, Any]) -> Result:
|
|
220
|
+
"""
|
|
221
|
+
Validate data against schema with comprehensive error reporting.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
data: Data to validate against schema
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Result object with validation outcome
|
|
228
|
+
"""
|
|
229
|
+
try:
|
|
230
|
+
# First, check for dangerous content
|
|
231
|
+
sanitized_data = self.validator.sanitize_json(data)
|
|
232
|
+
|
|
233
|
+
# Then validate structure
|
|
234
|
+
errors = self.validator.validate_json_structure(sanitized_data, self.schema)
|
|
235
|
+
|
|
236
|
+
if errors:
|
|
237
|
+
return Result.err(ValidationError(
|
|
238
|
+
ErrorCode.JSON_SCHEMA_MISMATCH,
|
|
239
|
+
f"Schema validation failed: {'; '.join(errors)}"
|
|
240
|
+
))
|
|
241
|
+
|
|
242
|
+
return Result.ok(sanitized_data)
|
|
243
|
+
except Exception as e:
|
|
244
|
+
return Result.err(ValidationError(
|
|
245
|
+
ErrorCode.JSON_PARSE_ERROR,
|
|
246
|
+
f"Validation error: {str(e)}"
|
|
247
|
+
))
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class RateLimiter:
|
|
251
|
+
"""
|
|
252
|
+
Production-grade token bucket rate limiter with sliding window support.
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
def __init__(self, rate: int = 100, per_seconds: int = 60):
|
|
256
|
+
self.rate = rate
|
|
257
|
+
self.per_seconds = per_seconds
|
|
258
|
+
self._buckets: Dict[str, Dict] = {}
|
|
259
|
+
import threading
|
|
260
|
+
self._lock = threading.Lock()
|
|
261
|
+
|
|
262
|
+
def is_allowed(self, key: str) -> bool:
|
|
263
|
+
"""
|
|
264
|
+
Check if request is allowed for given key with thread safety.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
key: Identifier for the rate limiting bucket
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
True if allowed, False if rate limited
|
|
271
|
+
"""
|
|
272
|
+
import time
|
|
273
|
+
now = time.time()
|
|
274
|
+
|
|
275
|
+
with self._lock:
|
|
276
|
+
if key not in self._buckets:
|
|
277
|
+
self._buckets[key] = {
|
|
278
|
+
"tokens": self.rate,
|
|
279
|
+
"last_update": now
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
bucket = self._buckets[key]
|
|
283
|
+
|
|
284
|
+
# Refill tokens based on time passed
|
|
285
|
+
time_passed = now - bucket["last_update"]
|
|
286
|
+
tokens_to_add = time_passed * (self.rate / self.per_seconds)
|
|
287
|
+
bucket["tokens"] = min(self.rate, bucket["tokens"] + tokens_to_add)
|
|
288
|
+
bucket["last_update"] = now
|
|
289
|
+
|
|
290
|
+
# Check if we have a token available
|
|
291
|
+
if bucket["tokens"] >= 1:
|
|
292
|
+
bucket["tokens"] -= 1
|
|
293
|
+
return True
|
|
294
|
+
|
|
295
|
+
return False
|
|
296
|
+
|
|
297
|
+
def get_retry_after(self, key: str) -> float:
|
|
298
|
+
"""
|
|
299
|
+
Get seconds until next request is allowed.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
key: Identifier for the rate limiting bucket
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Seconds until next request is allowed
|
|
306
|
+
"""
|
|
307
|
+
if key not in self._buckets:
|
|
308
|
+
return 0
|
|
309
|
+
|
|
310
|
+
with self._lock:
|
|
311
|
+
bucket = self._buckets[key]
|
|
312
|
+
if bucket["tokens"] >= 1:
|
|
313
|
+
return 0
|
|
314
|
+
|
|
315
|
+
tokens_needed = 1 - bucket["tokens"]
|
|
316
|
+
return tokens_needed * (self.per_seconds / self.rate)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class InputSanitizer:
|
|
320
|
+
"""
|
|
321
|
+
Comprehensive input sanitization system.
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
@classmethod
|
|
325
|
+
def sanitize_for_sql(cls, value: str) -> str:
|
|
326
|
+
"""Sanitize input for SQL queries."""
|
|
327
|
+
# Remove dangerous SQL keywords and characters
|
|
328
|
+
dangerous_sql = [
|
|
329
|
+
'DROP', 'DELETE', 'INSERT', 'UPDATE', 'CREATE', 'ALTER', 'EXEC',
|
|
330
|
+
'UNION', 'SELECT', 'WHERE', 'FROM', 'JOIN', '--', ';', '/*', '*/'
|
|
331
|
+
]
|
|
332
|
+
|
|
333
|
+
result = value
|
|
334
|
+
for sql_keyword in dangerous_sql:
|
|
335
|
+
# Case insensitive replacement
|
|
336
|
+
result = re.sub(sql_keyword, '', result, flags=re.IGNORECASE)
|
|
337
|
+
|
|
338
|
+
# Remove SQL comment patterns
|
|
339
|
+
result = re.sub(r'/\*.*?\*/', '', result) # Block comments
|
|
340
|
+
result = re.sub(r'--.*', '', result) # Line comments
|
|
341
|
+
|
|
342
|
+
return result.strip()
|
|
343
|
+
|
|
344
|
+
@classmethod
|
|
345
|
+
def sanitize_for_shell(cls, value: str) -> str:
|
|
346
|
+
"""Sanitize input for shell commands."""
|
|
347
|
+
# Remove shell metacharacters
|
|
348
|
+
dangerous_chars = [';', '|', '&', '`', '$', '(', ')', '<', '>']
|
|
349
|
+
result = value
|
|
350
|
+
for char in dangerous_chars:
|
|
351
|
+
result = result.replace(char, '')
|
|
352
|
+
|
|
353
|
+
return result.strip()
|
|
354
|
+
|
|
355
|
+
@classmethod
|
|
356
|
+
def sanitize_for_html(cls, value: str) -> str:
|
|
357
|
+
"""Sanitize input for HTML output."""
|
|
358
|
+
return html.escape(value, quote=True)
|
|
359
|
+
|
|
360
|
+
@classmethod
|
|
361
|
+
def sanitize_for_url(cls, value: str) -> str:
|
|
362
|
+
"""Sanitize input for URL usage."""
|
|
363
|
+
return urllib.parse.quote(value, safe='/:?#[]@!$&\'()*+,;=')
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# Global validator instance
|
|
367
|
+
_global_validator = None
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def get_validator() -> InputValidator:
|
|
371
|
+
"""Get the global input validator instance."""
|
|
372
|
+
global _global_validator
|
|
373
|
+
if _global_validator is None:
|
|
374
|
+
_global_validator = InputValidator()
|
|
375
|
+
return _global_validator
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def validate_input(value: str, pattern_name: str = None) -> Tuple[bool, str]:
|
|
379
|
+
"""
|
|
380
|
+
Convenience function to validate input.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
value: Value to validate
|
|
384
|
+
pattern_name: Optional pattern name to validate against
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
Tuple of (is_valid, message)
|
|
388
|
+
"""
|
|
389
|
+
validator = get_validator()
|
|
390
|
+
|
|
391
|
+
if pattern_name:
|
|
392
|
+
if validator.validate_pattern(value, pattern_name):
|
|
393
|
+
return True, "Valid input"
|
|
394
|
+
else:
|
|
395
|
+
return False, f"Does not match pattern: {pattern_name}"
|
|
396
|
+
|
|
397
|
+
is_safe, reason = validator.validate_dangerous_content(value)
|
|
398
|
+
if is_safe:
|
|
399
|
+
return True, "Valid input"
|
|
400
|
+
else:
|
|
401
|
+
return False, f"Contains dangerous content: {reason}"
|