44import os
55import re
66import urllib .parse
7- from collections .abc import Sequence
7+ from collections .abc import Coroutine , Sequence
88from pathlib import Path
9- from typing import Coroutine
109
1110import aiofiles
1211import orjson
1312from asgiref .compatibility import guarantee_single_callable
1413
1514from reactpy .backend ._common import (
1615 CLIENT_BUILD_DIR ,
17- traversal_safe_path ,
16+ safe_join_path ,
1817 vdom_head_elements_to_html ,
1918)
2019from reactpy .backend .hooks import ConnectionContext
2524from reactpy .core .serve import serve_layout
2625from reactpy .core .types import ComponentConstructor , VdomDict
2726
28- DEFAULT_STATIC_PATH = f"{ os .getcwd ()} /static"
2927DEFAULT_BLOCK_SIZE = 8192
3028_logger = logging .getLogger (__name__ )
3129
@@ -38,7 +36,7 @@ def __init__(
3836 dispatcher_path : str = "^reactpy/([^/]+)/?" ,
3937 js_modules_path : str | None = "^reactpy/modules/([^/]+)/?" ,
4038 static_path : str | None = "^reactpy/static/([^/]+)/?" ,
41- static_dir : str | None = DEFAULT_STATIC_PATH ,
39+ static_dir : Path | str | None = None ,
4240 head : Sequence [VdomDict ] | VdomDict | str = "" ,
4341 ) -> None :
4442 self .component = (
@@ -56,8 +54,8 @@ def __init__(
5654 "The first argument to `ReactPy` must be a component or an ASGI application."
5755 )
5856 self .dispatch_path = re .compile (dispatcher_path )
59- self .js_modules_path = re .compile (js_modules_path )
60- self .static_path = re .compile (static_path )
57+ self .js_modules_path = re .compile (js_modules_path ) if js_modules_path else None
58+ self .static_path = re .compile (static_path ) if static_path else None
6159 self .static_dir = static_dir
6260 self .all_paths = re .compile (
6361 "|" .join (
@@ -88,14 +86,20 @@ async def __call__(self, scope, receive, send) -> None:
8886 return
8987
9088 # Route requests to our JS web module app
91- if scope ["type" ] == "http" and re .match (
92- self .js_modules_path , scope ["path" ]
89+ if (
90+ scope ["type" ] == "http"
91+ and self .js_modules_path
92+ and re .match (self .js_modules_path , scope ["path" ])
9393 ):
9494 await self .js_modules_app (scope , receive , send )
9595 return
9696
9797 # Route requests to our static file server app
98- if scope ["type" ] == "http" and re .match (self .static_path , scope ["path" ]):
98+ if (
99+ scope ["type" ] == "http"
100+ and self .static_path
101+ and re .match (self .static_path , scope ["path" ])
102+ ):
99103 await self .static_file_app (scope , receive , send )
100104 return
101105
@@ -159,45 +163,42 @@ async def js_modules_app(self, scope, receive, send) -> None:
159163 """The ASGI application for ReactPy web modules."""
160164
161165 if not REACTPY_WEB_MODULES_DIR .current :
162- raise RuntimeError ("No web modules directory configured" )
163-
164- # Get the relative file path from the URL
165- file_url_path = re .match (self .js_modules_path , scope ["path" ])[1 ]
166+ raise RuntimeError ("No web modules directory configured." )
166167
167168 # Make sure the user hasn't tried to escape the web modules directory
168169 try :
169- file_path = traversal_safe_path (
170+ abs_file_path = safe_join_path (
170171 REACTPY_WEB_MODULES_DIR .current ,
171172 REACTPY_WEB_MODULES_DIR .current ,
172- file_url_path ,
173+ re . match ( self . js_modules_path , scope [ "path" ])[ 1 ] ,
173174 )
174175 except ValueError :
175176 await simple_response (send , 403 , "Forbidden" )
176177 return
177178
178179 # Serve the file
179- await file_response (scope , send , file_path )
180+ await file_response (scope , send , abs_file_path )
180181
181182 async def static_file_app (self , scope , receive , send ) -> None :
182183 """The ASGI application for ReactPy static files."""
183184
184- if self .static_dir is None :
185- raise RuntimeError ("No static directory configured" )
186-
187- # Get the relative file path from the URL
188- file_url_path = re .match (self .static_path , scope ["path" ])[1 ]
185+ if not self .static_dir :
186+ raise RuntimeError (
187+ "Static files cannot be served without defining `static_dir`."
188+ )
189189
190190 # Make sure the user hasn't tried to escape the static directory
191191 try :
192- file_path = traversal_safe_path (
193- self .static_dir , self .static_dir , file_url_path
192+ abs_file_path = safe_join_path (
193+ self .static_dir ,
194+ re .match (self .static_path , scope ["path" ])[1 ],
194195 )
195196 except ValueError :
196197 await simple_response (send , 403 , "Forbidden" )
197198 return
198199
199200 # Serve the file
200- await file_response (scope , send , file_path )
201+ await file_response (scope , send , abs_file_path )
201202
202203 async def index_html_app (self , scope , receive , send ) -> None :
203204 """The ASGI application for ReactPy index.html."""
@@ -289,7 +290,9 @@ async def file_response(scope, send, file_path: Path) -> None:
289290 )
290291 if mime_type is None :
291292 mime_type = "text/plain"
292- _logger .error (f"Could not determine MIME type for { file_path } ." )
293+ _logger .error (
294+ f"Could not determine MIME type for { file_path } . Defaulting to 'text/plain'."
295+ )
293296
294297 # Send the file in chunks
295298 async with aiofiles .open (file_path , "rb" ) as file_handle :
@@ -299,7 +302,12 @@ async def file_response(scope, send, file_path: Path) -> None:
299302 "status" : 200 ,
300303 "headers" : [
301304 (b"content-type" , mime_type .encode ()),
302- (b"last-modified" , str (os .path .getmtime (file_path )).encode ()),
305+ (
306+ b"last-modified" ,
307+ str (
308+ await asyncio .to_thread (os .path .getmtime , file_path )
309+ ).encode (),
310+ ),
303311 ],
304312 }
305313 )
0 commit comments