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
- file_path = Path('public/css') / filename
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
- file_path = Path('public/js') / filename
132
- if not file_path.exists():
133
- return Response(status_code=404)
134
- return FileResponse(file_path, media_type='application/javascript')
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
- file_path = Path('public/assets') / filename
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
- file_path = Path('public/uploads') / filename
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
- print(traceback.format_exc())
578
- error_message = str(exc)
579
- error_trace = traceback.format_exc() if not IS_PRODUCTION else None
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=os.getenv('AUTH_SECRET', 'change-me'),
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))