createsonline 0.1.26__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.
- createsonline/__init__.py +46 -0
- createsonline/admin/__init__.py +7 -0
- createsonline/admin/content.py +526 -0
- createsonline/admin/crud.py +805 -0
- createsonline/admin/field_builder.py +559 -0
- createsonline/admin/integration.py +482 -0
- createsonline/admin/interface.py +2562 -0
- createsonline/admin/model_creator.py +513 -0
- createsonline/admin/model_manager.py +388 -0
- createsonline/admin/modern_dashboard.py +498 -0
- createsonline/admin/permissions.py +264 -0
- createsonline/admin/user_forms.py +594 -0
- createsonline/ai/__init__.py +202 -0
- createsonline/ai/fields.py +1226 -0
- createsonline/ai/orm.py +325 -0
- createsonline/ai/services.py +1244 -0
- createsonline/app.py +506 -0
- createsonline/auth/__init__.py +8 -0
- createsonline/auth/management.py +228 -0
- createsonline/auth/models.py +552 -0
- createsonline/cli/__init__.py +5 -0
- createsonline/cli/commands/__init__.py +122 -0
- createsonline/cli/commands/database.py +416 -0
- createsonline/cli/commands/info.py +173 -0
- createsonline/cli/commands/initdb.py +218 -0
- createsonline/cli/commands/project.py +545 -0
- createsonline/cli/commands/serve.py +173 -0
- createsonline/cli/commands/shell.py +93 -0
- createsonline/cli/commands/users.py +148 -0
- createsonline/cli/main.py +2041 -0
- createsonline/cli/manage.py +274 -0
- createsonline/config/__init__.py +9 -0
- createsonline/config/app.py +2577 -0
- createsonline/config/database.py +179 -0
- createsonline/config/docs.py +384 -0
- createsonline/config/errors.py +160 -0
- createsonline/config/orm.py +43 -0
- createsonline/config/request.py +93 -0
- createsonline/config/settings.py +176 -0
- createsonline/data/__init__.py +23 -0
- createsonline/data/dataframe.py +925 -0
- createsonline/data/io.py +453 -0
- createsonline/data/series.py +557 -0
- createsonline/database/__init__.py +60 -0
- createsonline/database/abstraction.py +440 -0
- createsonline/database/assistant.py +585 -0
- createsonline/database/fields.py +442 -0
- createsonline/database/migrations.py +132 -0
- createsonline/database/models.py +604 -0
- createsonline/database.py +438 -0
- createsonline/http/__init__.py +28 -0
- createsonline/http/client.py +535 -0
- createsonline/ml/__init__.py +55 -0
- createsonline/ml/classification.py +552 -0
- createsonline/ml/clustering.py +680 -0
- createsonline/ml/metrics.py +542 -0
- createsonline/ml/neural.py +560 -0
- createsonline/ml/preprocessing.py +784 -0
- createsonline/ml/regression.py +501 -0
- createsonline/performance/__init__.py +19 -0
- createsonline/performance/cache.py +444 -0
- createsonline/performance/compression.py +335 -0
- createsonline/performance/core.py +419 -0
- createsonline/project_init.py +789 -0
- createsonline/routing.py +528 -0
- createsonline/security/__init__.py +34 -0
- createsonline/security/core.py +811 -0
- createsonline/security/encryption.py +349 -0
- createsonline/server.py +295 -0
- createsonline/static/css/admin.css +263 -0
- createsonline/static/css/common.css +358 -0
- createsonline/static/css/dashboard.css +89 -0
- createsonline/static/favicon.ico +0 -0
- createsonline/static/icons/icon-128x128.png +0 -0
- createsonline/static/icons/icon-128x128.webp +0 -0
- createsonline/static/icons/icon-16x16.png +0 -0
- createsonline/static/icons/icon-16x16.webp +0 -0
- createsonline/static/icons/icon-180x180.png +0 -0
- createsonline/static/icons/icon-180x180.webp +0 -0
- createsonline/static/icons/icon-192x192.png +0 -0
- createsonline/static/icons/icon-192x192.webp +0 -0
- createsonline/static/icons/icon-256x256.png +0 -0
- createsonline/static/icons/icon-256x256.webp +0 -0
- createsonline/static/icons/icon-32x32.png +0 -0
- createsonline/static/icons/icon-32x32.webp +0 -0
- createsonline/static/icons/icon-384x384.png +0 -0
- createsonline/static/icons/icon-384x384.webp +0 -0
- createsonline/static/icons/icon-48x48.png +0 -0
- createsonline/static/icons/icon-48x48.webp +0 -0
- createsonline/static/icons/icon-512x512.png +0 -0
- createsonline/static/icons/icon-512x512.webp +0 -0
- createsonline/static/icons/icon-64x64.png +0 -0
- createsonline/static/icons/icon-64x64.webp +0 -0
- createsonline/static/image/android-chrome-192x192.png +0 -0
- createsonline/static/image/android-chrome-512x512.png +0 -0
- createsonline/static/image/apple-touch-icon.png +0 -0
- createsonline/static/image/favicon-16x16.png +0 -0
- createsonline/static/image/favicon-32x32.png +0 -0
- createsonline/static/image/favicon.ico +0 -0
- createsonline/static/image/favicon.svg +17 -0
- createsonline/static/image/icon-128x128.png +0 -0
- createsonline/static/image/icon-128x128.webp +0 -0
- createsonline/static/image/icon-16x16.png +0 -0
- createsonline/static/image/icon-16x16.webp +0 -0
- createsonline/static/image/icon-180x180.png +0 -0
- createsonline/static/image/icon-180x180.webp +0 -0
- createsonline/static/image/icon-192x192.png +0 -0
- createsonline/static/image/icon-192x192.webp +0 -0
- createsonline/static/image/icon-256x256.png +0 -0
- createsonline/static/image/icon-256x256.webp +0 -0
- createsonline/static/image/icon-32x32.png +0 -0
- createsonline/static/image/icon-32x32.webp +0 -0
- createsonline/static/image/icon-384x384.png +0 -0
- createsonline/static/image/icon-384x384.webp +0 -0
- createsonline/static/image/icon-48x48.png +0 -0
- createsonline/static/image/icon-48x48.webp +0 -0
- createsonline/static/image/icon-512x512.png +0 -0
- createsonline/static/image/icon-512x512.webp +0 -0
- createsonline/static/image/icon-64x64.png +0 -0
- createsonline/static/image/icon-64x64.webp +0 -0
- createsonline/static/image/logo-header-h100.png +0 -0
- createsonline/static/image/logo-header-h100.webp +0 -0
- createsonline/static/image/logo-header-h200@2x.png +0 -0
- createsonline/static/image/logo-header-h200@2x.webp +0 -0
- createsonline/static/image/logo.png +0 -0
- createsonline/static/js/admin.js +274 -0
- createsonline/static/site.webmanifest +35 -0
- createsonline/static/templates/admin/base.html +87 -0
- createsonline/static/templates/admin/dashboard.html +217 -0
- createsonline/static/templates/admin/model_form.html +270 -0
- createsonline/static/templates/admin/model_list.html +202 -0
- createsonline/static/test_script.js +15 -0
- createsonline/static/test_styles.css +59 -0
- createsonline/static_files.py +365 -0
- createsonline/templates/404.html +100 -0
- createsonline/templates/admin_login.html +169 -0
- createsonline/templates/base.html +102 -0
- createsonline/templates/index.html +151 -0
- createsonline/templates.py +205 -0
- createsonline/testing.py +322 -0
- createsonline/utils.py +448 -0
- createsonline/validation/__init__.py +49 -0
- createsonline/validation/fields.py +598 -0
- createsonline/validation/models.py +504 -0
- createsonline/validation/validators.py +561 -0
- createsonline/views.py +184 -0
- createsonline-0.1.26.dist-info/METADATA +46 -0
- createsonline-0.1.26.dist-info/RECORD +152 -0
- createsonline-0.1.26.dist-info/WHEEL +5 -0
- createsonline-0.1.26.dist-info/entry_points.txt +2 -0
- createsonline-0.1.26.dist-info/licenses/LICENSE +21 -0
- createsonline-0.1.26.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
# createsonline/security/encryption.py
|
|
2
|
+
"""
|
|
3
|
+
CREATESONLINE Encryption Utilities
|
|
4
|
+
|
|
5
|
+
Military-grade encryption and password hashing:
|
|
6
|
+
- Argon2 password hashing
|
|
7
|
+
- AES-256 encryption
|
|
8
|
+
- Secure token generation
|
|
9
|
+
- Key derivation functions
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import hmac
|
|
14
|
+
import secrets
|
|
15
|
+
import base64
|
|
16
|
+
import time
|
|
17
|
+
from typing import Optional, Tuple, Dict, Any
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SecureHasher:
|
|
21
|
+
"""
|
|
22
|
+
Military-grade password hashing using Argon2-equivalent security
|
|
23
|
+
Falls back to PBKDF2 if Argon2 not available
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
# Try to import argon2 for best security
|
|
28
|
+
try:
|
|
29
|
+
import argon2
|
|
30
|
+
self.argon2_available = True
|
|
31
|
+
self.argon2_hasher = argon2.PasswordHasher(
|
|
32
|
+
time_cost=3, # Number of iterations
|
|
33
|
+
memory_cost=65536, # Memory usage in KiB
|
|
34
|
+
parallelism=1, # Number of parallel threads
|
|
35
|
+
hash_len=32, # Hash length
|
|
36
|
+
salt_len=16 # Salt length
|
|
37
|
+
)
|
|
38
|
+
pass
|
|
39
|
+
except ImportError:
|
|
40
|
+
self.argon2_available = False
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
def hash_password(self, password: str) -> str:
|
|
44
|
+
"""Hash password with maximum security"""
|
|
45
|
+
|
|
46
|
+
if self.argon2_available:
|
|
47
|
+
# Use Argon2 (recommended by OWASP)
|
|
48
|
+
return self.argon2_hasher.hash(password)
|
|
49
|
+
else:
|
|
50
|
+
# Fallback to PBKDF2 with high iteration count
|
|
51
|
+
salt = secrets.token_bytes(32)
|
|
52
|
+
iterations = 100000 # High iteration count for security
|
|
53
|
+
|
|
54
|
+
# Create PBKDF2 hash
|
|
55
|
+
password_hash = hashlib.pbkdf2_hmac(
|
|
56
|
+
'sha256',
|
|
57
|
+
password.encode('utf-8'),
|
|
58
|
+
salt,
|
|
59
|
+
iterations
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Encode as: algorithm$iterations$salt$hash
|
|
63
|
+
encoded_salt = base64.b64encode(salt).decode('ascii')
|
|
64
|
+
encoded_hash = base64.b64encode(password_hash).decode('ascii')
|
|
65
|
+
|
|
66
|
+
return f"pbkdf2_sha256${iterations}${encoded_salt}${encoded_hash}"
|
|
67
|
+
|
|
68
|
+
def verify_password(self, password: str, hashed: str) -> bool:
|
|
69
|
+
"""Verify password against hash"""
|
|
70
|
+
|
|
71
|
+
if self.argon2_available and not hashed.startswith('pbkdf2_'):
|
|
72
|
+
# Verify with Argon2
|
|
73
|
+
try:
|
|
74
|
+
self.argon2_hasher.verify(hashed, password)
|
|
75
|
+
return True
|
|
76
|
+
except:
|
|
77
|
+
return False
|
|
78
|
+
else:
|
|
79
|
+
# Verify with PBKDF2
|
|
80
|
+
try:
|
|
81
|
+
parts = hashed.split('$')
|
|
82
|
+
if len(parts) != 4 or parts[0] != 'pbkdf2_sha256':
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
iterations = int(parts[1])
|
|
86
|
+
salt = base64.b64decode(parts[2])
|
|
87
|
+
stored_hash = base64.b64decode(parts[3])
|
|
88
|
+
|
|
89
|
+
# Compute hash with same parameters
|
|
90
|
+
computed_hash = hashlib.pbkdf2_hmac(
|
|
91
|
+
'sha256',
|
|
92
|
+
password.encode('utf-8'),
|
|
93
|
+
salt,
|
|
94
|
+
iterations
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Constant-time comparison to prevent timing attacks
|
|
98
|
+
return hmac.compare_digest(stored_hash, computed_hash)
|
|
99
|
+
|
|
100
|
+
except (ValueError, IndexError):
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
def needs_rehash(self, hashed: str) -> bool:
|
|
104
|
+
"""Check if password needs rehashing with stronger parameters"""
|
|
105
|
+
|
|
106
|
+
if self.argon2_available:
|
|
107
|
+
if not hashed.startswith('pbkdf2_'):
|
|
108
|
+
# Already using Argon2
|
|
109
|
+
try:
|
|
110
|
+
return self.argon2_hasher.check_needs_rehash(hashed)
|
|
111
|
+
except:
|
|
112
|
+
return True
|
|
113
|
+
else:
|
|
114
|
+
# Upgrade from PBKDF2 to Argon2
|
|
115
|
+
return True
|
|
116
|
+
else:
|
|
117
|
+
# Check PBKDF2 iteration count
|
|
118
|
+
try:
|
|
119
|
+
parts = hashed.split('$')
|
|
120
|
+
if len(parts) == 4 and parts[0] == 'pbkdf2_sha256':
|
|
121
|
+
iterations = int(parts[1])
|
|
122
|
+
return iterations < 100000 # Upgrade if less than 100k iterations
|
|
123
|
+
except:
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class SecureTokenGenerator:
|
|
130
|
+
"""Generate cryptographically secure tokens"""
|
|
131
|
+
|
|
132
|
+
@staticmethod
|
|
133
|
+
def generate_token(length: int = 32) -> str:
|
|
134
|
+
"""Generate secure random token"""
|
|
135
|
+
return secrets.token_urlsafe(length)
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def generate_api_key() -> str:
|
|
139
|
+
"""Generate API key with prefix"""
|
|
140
|
+
prefix = "cos_" # CREATESONLINE prefix
|
|
141
|
+
token = secrets.token_urlsafe(32)
|
|
142
|
+
return f"{prefix}{token}"
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def generate_session_id() -> str:
|
|
146
|
+
"""Generate session ID"""
|
|
147
|
+
timestamp = str(int(time.time()))
|
|
148
|
+
random_part = secrets.token_urlsafe(24)
|
|
149
|
+
return f"{timestamp}_{random_part}"
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def generate_csrf_token(session_id: str, secret_key: str) -> str:
|
|
153
|
+
"""Generate CSRF token tied to session"""
|
|
154
|
+
timestamp = str(int(time.time()))
|
|
155
|
+
message = f"{session_id}:{timestamp}"
|
|
156
|
+
|
|
157
|
+
signature = hmac.new(
|
|
158
|
+
secret_key.encode(),
|
|
159
|
+
message.encode(),
|
|
160
|
+
hashlib.sha256
|
|
161
|
+
).hexdigest()
|
|
162
|
+
|
|
163
|
+
return f"{timestamp}:{signature}"
|
|
164
|
+
|
|
165
|
+
@staticmethod
|
|
166
|
+
def verify_csrf_token(token: str, session_id: str, secret_key: str,
|
|
167
|
+
max_age: int = 3600) -> bool:
|
|
168
|
+
"""Verify CSRF token"""
|
|
169
|
+
try:
|
|
170
|
+
timestamp_str, signature = token.split(':', 1)
|
|
171
|
+
timestamp = int(timestamp_str)
|
|
172
|
+
|
|
173
|
+
# Check token age
|
|
174
|
+
if time.time() - timestamp > max_age:
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
# Verify signature
|
|
178
|
+
message = f"{session_id}:{timestamp_str}"
|
|
179
|
+
expected_signature = hmac.new(
|
|
180
|
+
secret_key.encode(),
|
|
181
|
+
message.encode(),
|
|
182
|
+
hashlib.sha256
|
|
183
|
+
).hexdigest()
|
|
184
|
+
|
|
185
|
+
return hmac.compare_digest(signature, expected_signature)
|
|
186
|
+
|
|
187
|
+
except (ValueError, IndexError):
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class AESEncryption:
|
|
192
|
+
"""AES-256 encryption utilities"""
|
|
193
|
+
|
|
194
|
+
def __init__(self, key: Optional[bytes] = None):
|
|
195
|
+
if key is None:
|
|
196
|
+
key = secrets.token_bytes(32) # 256-bit key
|
|
197
|
+
elif len(key) != 32:
|
|
198
|
+
# Derive 256-bit key from provided key
|
|
199
|
+
key = hashlib.sha256(key).digest()
|
|
200
|
+
|
|
201
|
+
self.key = key
|
|
202
|
+
|
|
203
|
+
# Try to import cryptography library for AES
|
|
204
|
+
try:
|
|
205
|
+
from cryptography.fernet import Fernet
|
|
206
|
+
from cryptography.hazmat.primitives import hashes
|
|
207
|
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
208
|
+
|
|
209
|
+
# Create Fernet key from our key
|
|
210
|
+
kdf = PBKDF2HMAC(
|
|
211
|
+
algorithm=hashes.SHA256(),
|
|
212
|
+
length=32,
|
|
213
|
+
salt=b'createsonline_salt', # In production, use random salt
|
|
214
|
+
iterations=100000,
|
|
215
|
+
)
|
|
216
|
+
fernet_key = base64.urlsafe_b64encode(kdf.derive(self.key))
|
|
217
|
+
self.cipher = Fernet(fernet_key)
|
|
218
|
+
self.crypto_available = True
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
except ImportError:
|
|
222
|
+
self.crypto_available = False
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
def encrypt(self, data: str) -> str:
|
|
226
|
+
"""Encrypt string data"""
|
|
227
|
+
|
|
228
|
+
if self.crypto_available:
|
|
229
|
+
# Use Fernet (AES-256)
|
|
230
|
+
encrypted = self.cipher.encrypt(data.encode())
|
|
231
|
+
return base64.b64encode(encrypted).decode()
|
|
232
|
+
else:
|
|
233
|
+
# Fallback to XOR encryption
|
|
234
|
+
data_bytes = data.encode()
|
|
235
|
+
encrypted_bytes = bytearray()
|
|
236
|
+
|
|
237
|
+
for i, byte in enumerate(data_bytes):
|
|
238
|
+
key_byte = self.key[i % len(self.key)]
|
|
239
|
+
encrypted_bytes.append(byte ^ key_byte)
|
|
240
|
+
|
|
241
|
+
return base64.b64encode(encrypted_bytes).decode()
|
|
242
|
+
|
|
243
|
+
def decrypt(self, encrypted_data: str) -> str:
|
|
244
|
+
"""Decrypt string data"""
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
if self.crypto_available:
|
|
248
|
+
# Use Fernet (AES-256)
|
|
249
|
+
encrypted_bytes = base64.b64decode(encrypted_data)
|
|
250
|
+
decrypted = self.cipher.decrypt(encrypted_bytes)
|
|
251
|
+
return decrypted.decode()
|
|
252
|
+
else:
|
|
253
|
+
# Fallback to XOR decryption
|
|
254
|
+
encrypted_bytes = base64.b64decode(encrypted_data)
|
|
255
|
+
decrypted_bytes = bytearray()
|
|
256
|
+
|
|
257
|
+
for i, byte in enumerate(encrypted_bytes):
|
|
258
|
+
key_byte = self.key[i % len(self.key)]
|
|
259
|
+
decrypted_bytes.append(byte ^ key_byte)
|
|
260
|
+
|
|
261
|
+
return decrypted_bytes.decode()
|
|
262
|
+
|
|
263
|
+
except Exception:
|
|
264
|
+
raise ValueError("Invalid encrypted data")
|
|
265
|
+
|
|
266
|
+
def encrypt_dict(self, data: Dict[str, Any]) -> str:
|
|
267
|
+
"""Encrypt dictionary as JSON"""
|
|
268
|
+
import json
|
|
269
|
+
json_str = json.dumps(data, separators=(',', ':'))
|
|
270
|
+
return self.encrypt(json_str)
|
|
271
|
+
|
|
272
|
+
def decrypt_dict(self, encrypted_data: str) -> Dict[str, Any]:
|
|
273
|
+
"""Decrypt to dictionary"""
|
|
274
|
+
import json
|
|
275
|
+
json_str = self.decrypt(encrypted_data)
|
|
276
|
+
return json.loads(json_str)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class KeyDerivation:
|
|
280
|
+
"""Key derivation functions for secure key generation"""
|
|
281
|
+
|
|
282
|
+
@staticmethod
|
|
283
|
+
def derive_key(password: str, salt: Optional[bytes] = None,
|
|
284
|
+
length: int = 32, iterations: int = 100000) -> Tuple[bytes, bytes]:
|
|
285
|
+
"""Derive encryption key from password"""
|
|
286
|
+
|
|
287
|
+
if salt is None:
|
|
288
|
+
salt = secrets.token_bytes(32)
|
|
289
|
+
|
|
290
|
+
key = hashlib.pbkdf2_hmac(
|
|
291
|
+
'sha256',
|
|
292
|
+
password.encode(),
|
|
293
|
+
salt,
|
|
294
|
+
iterations,
|
|
295
|
+
dklen=length
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
return key, salt
|
|
299
|
+
|
|
300
|
+
@staticmethod
|
|
301
|
+
def derive_multiple_keys(master_key: bytes, labels: list,
|
|
302
|
+
length: int = 32) -> Dict[str, bytes]:
|
|
303
|
+
"""Derive multiple keys from master key using HKDF-like approach"""
|
|
304
|
+
|
|
305
|
+
keys = {}
|
|
306
|
+
|
|
307
|
+
for label in labels:
|
|
308
|
+
# Create unique key for each label
|
|
309
|
+
info = f"CREATESONLINE_{label}".encode()
|
|
310
|
+
|
|
311
|
+
# Simple HKDF-like derivation
|
|
312
|
+
prk = hmac.new(b'salt', master_key, hashlib.sha256).digest()
|
|
313
|
+
okm = hmac.new(prk, info + b'\x01', hashlib.sha256).digest()[:length]
|
|
314
|
+
|
|
315
|
+
keys[label] = okm
|
|
316
|
+
|
|
317
|
+
return keys
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# Global secure hasher instance
|
|
321
|
+
_secure_hasher = SecureHasher()
|
|
322
|
+
|
|
323
|
+
def encrypt_password(password: str) -> str:
|
|
324
|
+
"""Encrypt password with maximum security"""
|
|
325
|
+
return _secure_hasher.hash_password(password)
|
|
326
|
+
|
|
327
|
+
def verify_password(password: str, hashed: str) -> bool:
|
|
328
|
+
"""Verify password against hash"""
|
|
329
|
+
return _secure_hasher.verify_password(password, hashed)
|
|
330
|
+
|
|
331
|
+
def generate_secure_token(length: int = 32) -> str:
|
|
332
|
+
"""Generate cryptographically secure token"""
|
|
333
|
+
return SecureTokenGenerator.generate_token(length)
|
|
334
|
+
|
|
335
|
+
def generate_api_key() -> str:
|
|
336
|
+
"""Generate API key"""
|
|
337
|
+
return SecureTokenGenerator.generate_api_key()
|
|
338
|
+
|
|
339
|
+
def generate_session_id() -> str:
|
|
340
|
+
"""Generate session ID"""
|
|
341
|
+
return SecureTokenGenerator.generate_session_id()
|
|
342
|
+
|
|
343
|
+
def create_encryption_cipher(key: Optional[bytes] = None) -> AESEncryption:
|
|
344
|
+
"""Create AES encryption cipher"""
|
|
345
|
+
return AESEncryption(key)
|
|
346
|
+
|
|
347
|
+
def secure_compare(a: str, b: str) -> bool:
|
|
348
|
+
"""Constant-time string comparison to prevent timing attacks"""
|
|
349
|
+
return hmac.compare_digest(a.encode(), b.encode())
|
createsonline/server.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# createsonline/server.py
|
|
2
|
+
"""
|
|
3
|
+
CREATESONLINE Pure Python HTTP Server
|
|
4
|
+
|
|
5
|
+
Zero external dependencies - runs ASGI apps with just Python stdlib.
|
|
6
|
+
This eliminates the need for uvicorn.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import socket
|
|
11
|
+
import json
|
|
12
|
+
import time
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from typing import Callable
|
|
15
|
+
from urllib.parse import parse_qs, urlparse
|
|
16
|
+
import logging
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger("createsonline.server")
|
|
19
|
+
|
|
20
|
+
class CreatesonlineServer:
|
|
21
|
+
"""Pure Python HTTP server for ASGI applications"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, app: Callable, host: str = "127.0.0.1", port: int = 8000):
|
|
24
|
+
self.app = app
|
|
25
|
+
self.host = host
|
|
26
|
+
self.original_port = port
|
|
27
|
+
self.port = self._find_available_port(host, port)
|
|
28
|
+
self.server = None
|
|
29
|
+
|
|
30
|
+
# Notify if port changed
|
|
31
|
+
if self.port != self.original_port:
|
|
32
|
+
import logging
|
|
33
|
+
logger = logging.getLogger("createsonline")
|
|
34
|
+
logger.warning(f"Port {self.original_port} is busy, using port {self.port} instead")
|
|
35
|
+
|
|
36
|
+
def _find_available_port(self, host: str, start_port: int, max_attempts: int = 100) -> int:
|
|
37
|
+
"""Find an available port starting from start_port"""
|
|
38
|
+
for port in range(start_port, start_port + max_attempts):
|
|
39
|
+
try:
|
|
40
|
+
# Try to bind to the port
|
|
41
|
+
test_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
42
|
+
test_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
43
|
+
test_socket.bind((host, port))
|
|
44
|
+
test_socket.close()
|
|
45
|
+
return port
|
|
46
|
+
except OSError:
|
|
47
|
+
# Port is in use, try next one
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
# If no port found, raise error
|
|
51
|
+
raise RuntimeError(f"No available ports found between {start_port} and {start_port + max_attempts}")
|
|
52
|
+
|
|
53
|
+
async def handle_client(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
|
|
54
|
+
"""Handle individual client connection"""
|
|
55
|
+
start_time = time.time()
|
|
56
|
+
response_size = 0
|
|
57
|
+
status_code = 500
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
# Read HTTP request
|
|
61
|
+
request_line = await reader.readline()
|
|
62
|
+
if not request_line:
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
request_line = request_line.decode('utf-8').strip()
|
|
66
|
+
method, path, _ = request_line.split(' ', 2)
|
|
67
|
+
|
|
68
|
+
# Read headers
|
|
69
|
+
headers = {}
|
|
70
|
+
while True:
|
|
71
|
+
line = await reader.readline()
|
|
72
|
+
if line == b'\r\n':
|
|
73
|
+
break
|
|
74
|
+
if line:
|
|
75
|
+
key, value = line.decode('utf-8').strip().split(':', 1)
|
|
76
|
+
headers[key.lower()] = value.strip()
|
|
77
|
+
|
|
78
|
+
# Read body if present
|
|
79
|
+
body = b''
|
|
80
|
+
if 'content-length' in headers:
|
|
81
|
+
content_length = int(headers['content-length'])
|
|
82
|
+
body = await reader.readexactly(content_length)
|
|
83
|
+
|
|
84
|
+
# Parse URL
|
|
85
|
+
parsed_url = urlparse(path)
|
|
86
|
+
query_string = parsed_url.query.encode() if parsed_url.query else b''
|
|
87
|
+
|
|
88
|
+
# Build ASGI scope
|
|
89
|
+
scope = {
|
|
90
|
+
'type': 'http',
|
|
91
|
+
'asgi': {'version': '3.0'},
|
|
92
|
+
'http_version': '1.1',
|
|
93
|
+
'method': method,
|
|
94
|
+
'scheme': 'http',
|
|
95
|
+
'path': parsed_url.path,
|
|
96
|
+
'query_string': query_string,
|
|
97
|
+
'root_path': '',
|
|
98
|
+
'headers': [[k.encode(), v.encode()] for k, v in headers.items()],
|
|
99
|
+
'server': (self.host, self.port),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# ASGI receive callable
|
|
103
|
+
body_sent = False
|
|
104
|
+
async def receive():
|
|
105
|
+
nonlocal body_sent
|
|
106
|
+
if not body_sent:
|
|
107
|
+
body_sent = True
|
|
108
|
+
return {
|
|
109
|
+
'type': 'http.request',
|
|
110
|
+
'body': body,
|
|
111
|
+
'more_body': False,
|
|
112
|
+
}
|
|
113
|
+
return {'type': 'http.disconnect'}
|
|
114
|
+
|
|
115
|
+
# ASGI send callable
|
|
116
|
+
response_started = False
|
|
117
|
+
async def send(message):
|
|
118
|
+
nonlocal response_started, response_size, status_code
|
|
119
|
+
|
|
120
|
+
if message['type'] == 'http.response.start':
|
|
121
|
+
response_started = True
|
|
122
|
+
status_code = message['status']
|
|
123
|
+
headers = message.get('headers', [])
|
|
124
|
+
|
|
125
|
+
# Write status line
|
|
126
|
+
writer.write(f'HTTP/1.1 {status_code} OK\r\n'.encode())
|
|
127
|
+
|
|
128
|
+
# Write headers
|
|
129
|
+
for name, value in headers:
|
|
130
|
+
writer.write(f'{name.decode()}: {value.decode()}\r\n'.encode())
|
|
131
|
+
writer.write(b'\r\n')
|
|
132
|
+
|
|
133
|
+
elif message['type'] == 'http.response.body':
|
|
134
|
+
body = message.get('body', b'')
|
|
135
|
+
if body:
|
|
136
|
+
response_size += len(body)
|
|
137
|
+
writer.write(body)
|
|
138
|
+
await writer.drain()
|
|
139
|
+
|
|
140
|
+
# Call ASGI app
|
|
141
|
+
await self.app(scope, receive, send)
|
|
142
|
+
|
|
143
|
+
# Log request after completion
|
|
144
|
+
elapsed_ms = (time.time() - start_time) * 1000
|
|
145
|
+
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
146
|
+
status_indicator = self._get_status_indicator(status_code)
|
|
147
|
+
|
|
148
|
+
# Format size
|
|
149
|
+
if response_size < 1024:
|
|
150
|
+
size_str = f"{response_size}B"
|
|
151
|
+
elif response_size < 1024 * 1024:
|
|
152
|
+
size_str = f"{response_size / 1024:.1f}KB"
|
|
153
|
+
else:
|
|
154
|
+
size_str = f"{response_size / (1024 * 1024):.1f}MB"
|
|
155
|
+
|
|
156
|
+
# Color-coded status for better visibility
|
|
157
|
+
status_display = f"{status_code}"
|
|
158
|
+
logger.info(f"[{timestamp}] [{status_indicator}] {method:<6} {path:<50} {status_display:<4} {size_str:>8} {elapsed_ms:>5.0f}ms")
|
|
159
|
+
except Exception as e:
|
|
160
|
+
# Log error to console
|
|
161
|
+
elapsed_ms = (time.time() - start_time) * 1000
|
|
162
|
+
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
163
|
+
logger.error(f"[{timestamp}] [XXX] {method:<6} {path:<50} 500 ERROR {elapsed_ms:>5.0f}ms | {str(e)}")
|
|
164
|
+
# Send 500 error
|
|
165
|
+
error_response = json.dumps({
|
|
166
|
+
"error": "Internal Server Error",
|
|
167
|
+
"message": str(e)
|
|
168
|
+
}).encode()
|
|
169
|
+
|
|
170
|
+
response = (
|
|
171
|
+
b'HTTP/1.1 500 Internal Server Error\r\n'
|
|
172
|
+
b'Content-Type: application/json\r\n'
|
|
173
|
+
b'Content-Length: ' + str(len(error_response)).encode() + b'\r\n'
|
|
174
|
+
b'\r\n'
|
|
175
|
+
) + error_response
|
|
176
|
+
|
|
177
|
+
writer.write(response)
|
|
178
|
+
await writer.drain()
|
|
179
|
+
|
|
180
|
+
finally:
|
|
181
|
+
writer.close()
|
|
182
|
+
await writer.wait_closed()
|
|
183
|
+
|
|
184
|
+
def _get_status_indicator(self, status: int) -> str:
|
|
185
|
+
"""Get visual indicator for HTTP status code"""
|
|
186
|
+
if 200 <= status < 300:
|
|
187
|
+
return "OK " # Success
|
|
188
|
+
elif 300 <= status < 400:
|
|
189
|
+
return "->>" # Redirect
|
|
190
|
+
elif 400 <= status < 500:
|
|
191
|
+
return "ERR" # Client error
|
|
192
|
+
else:
|
|
193
|
+
return "XXX" # Server error
|
|
194
|
+
|
|
195
|
+
async def serve(self):
|
|
196
|
+
"""Start the server"""
|
|
197
|
+
try:
|
|
198
|
+
self.server = await asyncio.start_server(
|
|
199
|
+
self.handle_client,
|
|
200
|
+
self.host,
|
|
201
|
+
self.port
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Print server info
|
|
205
|
+
from . import __version__
|
|
206
|
+
startup_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
207
|
+
print("\n" + "=" * 70)
|
|
208
|
+
print(f" CREATESONLINE v{__version__} - AI-Native Web Framework")
|
|
209
|
+
print("=" * 70)
|
|
210
|
+
print(f" Started: {startup_time}")
|
|
211
|
+
print(f" Server URL: http://{self.host}:{self.port}")
|
|
212
|
+
print(f" Press CTRL+C to stop")
|
|
213
|
+
print("=" * 70)
|
|
214
|
+
print(f"\n{'TIME':<12} {'STATUS':<7} {'METHOD':<8} {'PATH':<50} {'CODE':<6} {'SIZE':<10} {'TIME'}")
|
|
215
|
+
print("-" * 120)
|
|
216
|
+
except OSError as e:
|
|
217
|
+
logger.error(f"Failed to start server on {self.host}:{self.port}")
|
|
218
|
+
logger.error(f"Error: {e}")
|
|
219
|
+
raise
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
async with self.server:
|
|
223
|
+
await self.server.serve_forever()
|
|
224
|
+
|
|
225
|
+
def run(self):
|
|
226
|
+
"""Run the server (blocking)"""
|
|
227
|
+
try:
|
|
228
|
+
asyncio.run(self.serve())
|
|
229
|
+
except KeyboardInterrupt:
|
|
230
|
+
logger.info("Server stopped")
|
|
231
|
+
def run_server(app: Callable, host: str = "127.0.0.1", port: int = 8000, reload: bool = False):
|
|
232
|
+
"""
|
|
233
|
+
Run CREATESONLINE pure Python server
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
app: ASGI application callable
|
|
237
|
+
host: Host to bind to
|
|
238
|
+
port: Port to listen on
|
|
239
|
+
reload: Enable auto-reload on file changes
|
|
240
|
+
"""
|
|
241
|
+
# Configure logging
|
|
242
|
+
logging.basicConfig(
|
|
243
|
+
level=logging.INFO,
|
|
244
|
+
format='%(message)s'
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
if reload:
|
|
248
|
+
# Use watchdog for auto-reload
|
|
249
|
+
try:
|
|
250
|
+
import sys
|
|
251
|
+
import subprocess
|
|
252
|
+
from pathlib import Path
|
|
253
|
+
from watchdog.observers import Observer
|
|
254
|
+
from watchdog.events import FileSystemEventHandler
|
|
255
|
+
|
|
256
|
+
class ReloadHandler(FileSystemEventHandler):
|
|
257
|
+
def __init__(self):
|
|
258
|
+
self.process = None
|
|
259
|
+
self.restart_server()
|
|
260
|
+
|
|
261
|
+
def restart_server(self):
|
|
262
|
+
if self.process:
|
|
263
|
+
self.process.terminate()
|
|
264
|
+
self.process.wait()
|
|
265
|
+
|
|
266
|
+
logger.info("🔄 Restarting server...")
|
|
267
|
+
self.process = subprocess.Popen([sys.executable] + sys.argv)
|
|
268
|
+
|
|
269
|
+
def on_modified(self, event):
|
|
270
|
+
if event.src_path.endswith('.py'):
|
|
271
|
+
logger.info(f"📝 File changed: {event.src_path}")
|
|
272
|
+
self.restart_server()
|
|
273
|
+
|
|
274
|
+
handler = ReloadHandler()
|
|
275
|
+
observer = Observer()
|
|
276
|
+
observer.schedule(handler, path=Path.cwd(), recursive=True)
|
|
277
|
+
observer.start()
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
observer.join()
|
|
281
|
+
except KeyboardInterrupt:
|
|
282
|
+
observer.stop()
|
|
283
|
+
if handler.process:
|
|
284
|
+
handler.process.terminate()
|
|
285
|
+
observer.join()
|
|
286
|
+
|
|
287
|
+
except ImportError:
|
|
288
|
+
logger.warning("⚠️ watchdog not installed, auto-reload disabled")
|
|
289
|
+
logger.info("Install with: pip install watchdog")
|
|
290
|
+
server = CreatesonlineServer(app, host, port)
|
|
291
|
+
server.run()
|
|
292
|
+
else:
|
|
293
|
+
server = CreatesonlineServer(app, host, port)
|
|
294
|
+
server.run()
|
|
295
|
+
|