create-caspian-app 0.3.0-rc.2 → 0.3.0-rc.21
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.
- package/dist/.github/copilot-instructions.md +33 -3
- package/dist/AGENTS.md +17 -4
- package/dist/caspian.js +1 -1
- package/dist/index.js +1 -1
- package/dist/main-websocket.py +1001 -0
- package/dist/main.py +86 -6
- package/dist/public/js/pp-reactive-v2.js +1 -1
- package/dist/pyproject.toml +1 -1
- package/dist/settings/bs-config.json +6 -6
- package/dist/settings/bs-config.ts +132 -62
- package/dist/settings/component-map.ts +6 -6
- package/dist/settings/python-server.ts +8 -18
- package/dist/settings/utils.ts +3 -3
- package/dist/src/lib/auth/auth_config.py +1 -1
- package/dist/src/lib/mcp/mcp_server.py +8 -8
- package/dist/src/lib/websocket/websocket_security.py +84 -0
- package/package.json +1 -1
package/dist/main.py
CHANGED
|
@@ -87,6 +87,11 @@ REQUEST_TIMEOUT_SECONDS = max(
|
|
|
87
87
|
1.0,
|
|
88
88
|
float(os.getenv('CASPIAN_REQUEST_TIMEOUT_SECONDS', 20)),
|
|
89
89
|
)
|
|
90
|
+
MAX_CONTENT_LENGTH_BYTES = max(1, MAX_CONTENT_LENGTH_MB) * 1024 * 1024
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class RequestBodyTooLarge(Exception):
|
|
94
|
+
pass
|
|
90
95
|
|
|
91
96
|
|
|
92
97
|
def _client_error_message(exc: Exception) -> str:
|
|
@@ -228,6 +233,67 @@ class SecurityHeadersMiddleware:
|
|
|
228
233
|
await self.app(scope, receive, send_wrapper)
|
|
229
234
|
|
|
230
235
|
|
|
236
|
+
class BodySizeLimitMiddleware:
|
|
237
|
+
"""Reject oversized HTTP request bodies before route or RPC parsing."""
|
|
238
|
+
|
|
239
|
+
def __init__(self, app: ASGIApp): self.app = app
|
|
240
|
+
|
|
241
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send):
|
|
242
|
+
if scope["type"] != "http":
|
|
243
|
+
await self.app(scope, receive, send)
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
headers = MutableHeaders(scope=scope)
|
|
247
|
+
content_length = headers.get("content-length")
|
|
248
|
+
if content_length:
|
|
249
|
+
try:
|
|
250
|
+
if int(content_length) > MAX_CONTENT_LENGTH_BYTES:
|
|
251
|
+
await self._send_too_large(send)
|
|
252
|
+
return
|
|
253
|
+
except ValueError:
|
|
254
|
+
pass
|
|
255
|
+
|
|
256
|
+
received = 0
|
|
257
|
+
response_started = False
|
|
258
|
+
|
|
259
|
+
async def limited_receive():
|
|
260
|
+
nonlocal received
|
|
261
|
+
message = await receive()
|
|
262
|
+
if message["type"] == "http.request":
|
|
263
|
+
received += len(message.get("body", b""))
|
|
264
|
+
if received > MAX_CONTENT_LENGTH_BYTES:
|
|
265
|
+
raise RequestBodyTooLarge()
|
|
266
|
+
return message
|
|
267
|
+
|
|
268
|
+
async def send_wrapper(message):
|
|
269
|
+
nonlocal response_started
|
|
270
|
+
if message["type"] == "http.response.start":
|
|
271
|
+
response_started = True
|
|
272
|
+
await send(message)
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
await self.app(scope, limited_receive, send_wrapper)
|
|
276
|
+
except RequestBodyTooLarge:
|
|
277
|
+
if not response_started:
|
|
278
|
+
await self._send_too_large(send)
|
|
279
|
+
|
|
280
|
+
async def _send_too_large(self, send: Send):
|
|
281
|
+
response = Response(
|
|
282
|
+
content="Request body too large.",
|
|
283
|
+
status_code=413,
|
|
284
|
+
media_type="text/plain",
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
async def receive_empty_body():
|
|
288
|
+
return {"type": "http.request", "body": b"", "more_body": False}
|
|
289
|
+
|
|
290
|
+
await response(
|
|
291
|
+
{"type": "http", "method": "POST", "path": "/", "headers": []},
|
|
292
|
+
receive=receive_empty_body,
|
|
293
|
+
send=send,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
231
297
|
class AuthMiddleware:
|
|
232
298
|
"""Auth middleware using pure ASGI pattern for proper session handling."""
|
|
233
299
|
|
|
@@ -318,7 +384,8 @@ class RequestDiagnosticsMiddleware:
|
|
|
318
384
|
|
|
319
385
|
method = scope.get("method", "GET")
|
|
320
386
|
path = scope.get("path", "")
|
|
321
|
-
should_log = not path.startswith(
|
|
387
|
+
should_log = not path.startswith(
|
|
388
|
+
('/css/', '/js/', '/assets/', '/favicon.ico'))
|
|
322
389
|
started = time.perf_counter()
|
|
323
390
|
|
|
324
391
|
if should_log and not IS_PRODUCTION:
|
|
@@ -349,12 +416,14 @@ class RequestDiagnosticsMiddleware:
|
|
|
349
416
|
except Exception:
|
|
350
417
|
if should_log and not IS_PRODUCTION:
|
|
351
418
|
elapsed_ms = int((time.perf_counter() - started) * 1000)
|
|
352
|
-
print(
|
|
419
|
+
print(
|
|
420
|
+
f"[request:error] {method} {path} after {elapsed_ms}ms", flush=True)
|
|
353
421
|
raise
|
|
354
422
|
finally:
|
|
355
423
|
if should_log and not IS_PRODUCTION:
|
|
356
424
|
elapsed_ms = int((time.perf_counter() - started) * 1000)
|
|
357
|
-
print(
|
|
425
|
+
print(
|
|
426
|
+
f"[request:end] {method} {path} {elapsed_ms}ms", flush=True)
|
|
358
427
|
|
|
359
428
|
# ====
|
|
360
429
|
# Route Registration
|
|
@@ -674,7 +743,8 @@ async def custom_general_exception_handler(request: Request, exc: Exception):
|
|
|
674
743
|
context_data = {'request': request,
|
|
675
744
|
'error_message': error_message, 'error_trace': error_trace}
|
|
676
745
|
try:
|
|
677
|
-
rendered_content = compile_template(
|
|
746
|
+
rendered_content = compile_template(
|
|
747
|
+
raw_content).render(**context_data)
|
|
678
748
|
html_output, root_layout_id = await render_with_nested_layouts(
|
|
679
749
|
children=rendered_content,
|
|
680
750
|
route_dir='src/app',
|
|
@@ -714,9 +784,19 @@ app.add_middleware(
|
|
|
714
784
|
https_only=IS_PRODUCTION,
|
|
715
785
|
path='/',
|
|
716
786
|
)
|
|
787
|
+
app.add_middleware(BodySizeLimitMiddleware)
|
|
717
788
|
app.add_middleware(SecurityHeadersMiddleware)
|
|
718
|
-
|
|
789
|
+
|
|
790
|
+
if not IS_PRODUCTION:
|
|
791
|
+
app.add_middleware(RequestDiagnosticsMiddleware)
|
|
719
792
|
|
|
720
793
|
if __name__ == '__main__':
|
|
721
794
|
port = int(os.getenv('PORT', 5091))
|
|
722
|
-
|
|
795
|
+
workers = max(1, int(os.getenv('UVICORN_WORKERS', '1')))
|
|
796
|
+
uvicorn.run(
|
|
797
|
+
"main:app",
|
|
798
|
+
host="0.0.0.0",
|
|
799
|
+
port=port,
|
|
800
|
+
reload=False,
|
|
801
|
+
workers=workers,
|
|
802
|
+
)
|