create-caspian-app 0.2.0-beta.90 → 0.2.0-beta.92
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/main.py
CHANGED
|
@@ -3,13 +3,13 @@ from casp.scripts_type import transform_scripts
|
|
|
3
3
|
import inspect
|
|
4
4
|
import os
|
|
5
5
|
import importlib.util
|
|
6
|
-
import mimetypes
|
|
7
6
|
import secrets
|
|
8
7
|
import traceback
|
|
9
8
|
import json
|
|
10
9
|
from pathlib import Path
|
|
11
10
|
from fastapi import FastAPI, Request, Response
|
|
12
11
|
from fastapi.responses import RedirectResponse, FileResponse, HTMLResponse
|
|
12
|
+
from starlette.datastructures import MutableHeaders
|
|
13
13
|
from starlette.middleware.sessions import SessionMiddleware
|
|
14
14
|
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
15
15
|
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
@@ -38,6 +38,12 @@ from casp.streaming import SSE
|
|
|
38
38
|
from typing import Any, Optional, get_args, get_origin, Union
|
|
39
39
|
from urllib.parse import urlparse
|
|
40
40
|
from src.lib.auth.auth_config import build_auth_settings
|
|
41
|
+
from src.lib.security.runtime_security import (
|
|
42
|
+
build_security_headers,
|
|
43
|
+
client_error_message,
|
|
44
|
+
get_session_secret,
|
|
45
|
+
public_file_response,
|
|
46
|
+
)
|
|
41
47
|
|
|
42
48
|
load_dotenv()
|
|
43
49
|
cfg = get_config()
|
|
@@ -77,6 +83,18 @@ CACHE_ENABLED = os.getenv('CACHE_ENABLED', 'false').lower() == 'true'
|
|
|
77
83
|
DEFAULT_TTL = int(os.getenv('CACHE_TTL', 600))
|
|
78
84
|
|
|
79
85
|
|
|
86
|
+
def _client_error_message(exc: Exception) -> str:
|
|
87
|
+
return client_error_message(exc, is_production=IS_PRODUCTION)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _get_session_secret() -> str:
|
|
91
|
+
return get_session_secret(is_production=IS_PRODUCTION)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _build_security_headers() -> dict[str, str]:
|
|
95
|
+
return build_security_headers(is_production=IS_PRODUCTION)
|
|
96
|
+
|
|
97
|
+
|
|
80
98
|
def _dev_cookie_scope() -> str:
|
|
81
99
|
if IS_PRODUCTION:
|
|
82
100
|
return ""
|
|
@@ -120,36 +138,26 @@ SESSION_COOKIE_NAME = _scoped_cookie_name(
|
|
|
120
138
|
|
|
121
139
|
@app.get('/css/{filename:path}')
|
|
122
140
|
async def serve_css(filename: str):
|
|
123
|
-
|
|
124
|
-
if not file_path.exists():
|
|
125
|
-
return Response(status_code=404)
|
|
126
|
-
return FileResponse(file_path, media_type='text/css')
|
|
141
|
+
return public_file_response('public/css', filename, media_type='text/css')
|
|
127
142
|
|
|
128
143
|
|
|
129
144
|
@app.get('/js/{filename:path}')
|
|
130
145
|
async def serve_js(filename: str):
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
146
|
+
return public_file_response(
|
|
147
|
+
'public/js',
|
|
148
|
+
filename,
|
|
149
|
+
media_type='application/javascript',
|
|
150
|
+
)
|
|
135
151
|
|
|
136
152
|
|
|
137
153
|
@app.get('/assets/{filename:path}')
|
|
138
154
|
async def serve_assets(filename: str):
|
|
139
|
-
|
|
140
|
-
if not file_path.exists():
|
|
141
|
-
return Response(status_code=404)
|
|
142
|
-
mime_type, _ = mimetypes.guess_type(str(file_path))
|
|
143
|
-
return FileResponse(file_path, media_type=mime_type or 'application/octet-stream')
|
|
155
|
+
return public_file_response('public/assets', filename)
|
|
144
156
|
|
|
145
157
|
|
|
146
158
|
@app.get('/uploads/{filename:path}')
|
|
147
159
|
async def serve_uploads(filename: str):
|
|
148
|
-
|
|
149
|
-
if not file_path.exists():
|
|
150
|
-
return Response(status_code=404)
|
|
151
|
-
mime_type, _ = mimetypes.guess_type(str(file_path))
|
|
152
|
-
return FileResponse(file_path, media_type=mime_type or 'application/octet-stream')
|
|
160
|
+
return public_file_response('public/uploads', filename)
|
|
153
161
|
|
|
154
162
|
|
|
155
163
|
@app.get('/favicon.ico')
|
|
@@ -191,6 +199,29 @@ class CSRFMiddleware:
|
|
|
191
199
|
await self.app(scope, receive, send_wrapper)
|
|
192
200
|
|
|
193
201
|
|
|
202
|
+
class SecurityHeadersMiddleware:
|
|
203
|
+
"""Attach baseline browser security headers to HTTP responses."""
|
|
204
|
+
|
|
205
|
+
def __init__(self, app: ASGIApp): self.app = app
|
|
206
|
+
|
|
207
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send):
|
|
208
|
+
if scope["type"] != "http":
|
|
209
|
+
await self.app(scope, receive, send)
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
async def send_wrapper(message):
|
|
213
|
+
if message["type"] == "http.response.start":
|
|
214
|
+
raw_headers = list(message.get("headers", []))
|
|
215
|
+
headers = MutableHeaders(raw=raw_headers)
|
|
216
|
+
for name, value in _build_security_headers().items():
|
|
217
|
+
if headers.get(name) is None:
|
|
218
|
+
headers[name] = value
|
|
219
|
+
message = {**message, "headers": raw_headers}
|
|
220
|
+
await send(message)
|
|
221
|
+
|
|
222
|
+
await self.app(scope, receive, send_wrapper)
|
|
223
|
+
|
|
224
|
+
|
|
194
225
|
class AuthMiddleware:
|
|
195
226
|
"""Auth middleware using pure ASGI pattern for proper session handling."""
|
|
196
227
|
|
|
@@ -574,9 +605,10 @@ async def custom_404_handler(request: Request, exc: StarletteHTTPException):
|
|
|
574
605
|
|
|
575
606
|
@app.exception_handler(Exception)
|
|
576
607
|
async def custom_general_exception_handler(request: Request, exc: Exception):
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
608
|
+
full_trace = traceback.format_exc()
|
|
609
|
+
print(full_trace)
|
|
610
|
+
error_message = _client_error_message(exc)
|
|
611
|
+
error_trace = full_trace if not IS_PRODUCTION else None
|
|
580
612
|
|
|
581
613
|
error_page_path = os.path.join('src', 'app', 'error.html')
|
|
582
614
|
if os.path.exists(error_page_path):
|
|
@@ -619,13 +651,14 @@ app.add_middleware(CSRFMiddleware)
|
|
|
619
651
|
|
|
620
652
|
app.add_middleware(
|
|
621
653
|
SessionMiddleware,
|
|
622
|
-
secret_key=
|
|
654
|
+
secret_key=_get_session_secret(),
|
|
623
655
|
session_cookie=SESSION_COOKIE_NAME,
|
|
624
656
|
max_age=SESSION_LIFETIME_HOURS * 3600,
|
|
625
657
|
same_site='lax',
|
|
626
658
|
https_only=IS_PRODUCTION,
|
|
627
659
|
path='/',
|
|
628
660
|
)
|
|
661
|
+
app.add_middleware(SecurityHeadersMiddleware)
|
|
629
662
|
|
|
630
663
|
if __name__ == '__main__':
|
|
631
664
|
port = int(os.getenv('PORT', 5091))
|