Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove blocking IO for static resources and refactor exception handling #8507

Merged
merged 13 commits into from
Jul 21, 2024
1 change: 1 addition & 0 deletions CHANGES/8507.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed blocking IO in the event loop for static resources and refactored exception handling -- by :user:`steverep`.
steverep marked this conversation as resolved.
Show resolved Hide resolved
80 changes: 44 additions & 36 deletions aiohttp/web_urldispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -639,59 +639,67 @@

async def _handle(self, request: Request) -> StreamResponse:
rel_url = request.match_info["filename"]
filename = Path(rel_url)
if filename.anchor:
# rel_url is an absolute name like
# /static/\\machine_name\c$ or /static/D:\path
# where the static dir is totally different
raise HTTPForbidden()

unresolved_path = self._directory.joinpath(filename)
loop = asyncio.get_event_loop()
steverep marked this conversation as resolved.
Show resolved Hide resolved
return await loop.run_in_executor(
None, self._resolve_path_to_response, unresolved_path
)

def _resolve_path_to_response(self, unresolved_path: Path) -> StreamResponse:
"""Takes the unresolved path and queries the file system to form a response."""
steverep marked this conversation as resolved.
Show resolved Hide resolved
# Check for access outside the root directory. For follow symlinks, URI
# cannot traverse out, but symlinks can. Otherwise, no access outside
# root is permitted.
try:
filename = Path(rel_url)
if filename.anchor:
# rel_url is an absolute name like
# /static/\\machine_name\c$ or /static/D:\path
# where the static dir is totally different
raise HTTPForbidden()
unresolved_path = self._directory.joinpath(filename)
if self._follow_symlinks:
bdraco marked this conversation as resolved.
Show resolved Hide resolved
normalized_path = Path(os.path.normpath(unresolved_path))
normalized_path.relative_to(self._directory)
filepath = normalized_path.resolve()
file_path = normalized_path.resolve()
else:
filepath = unresolved_path.resolve()
filepath.relative_to(self._directory)
except (ValueError, FileNotFoundError) as error:
# relatively safe
raise HTTPNotFound() from error
except HTTPForbidden:
raise
except Exception as error:
# perm error or other kind!
request.app.logger.exception(error)
file_path = unresolved_path.resolve()
file_path.relative_to(self._directory)
# TODO(PY39): Remove OSError to appease Windows
steverep marked this conversation as resolved.
Show resolved Hide resolved
except (ValueError, RuntimeError, OSError) as error:
# ValueError for relative check; RuntimeError for circular symlink.
raise HTTPNotFound() from error

# on opening a dir, load its contents if allowed
if filepath.is_dir():
if self._show_index:
try:
# if path is a directory, return the contents if permitted. Note the
# directory check will raise if a segment is not readable.
try:
if file_path.is_dir():
steverep marked this conversation as resolved.
Show resolved Hide resolved
if self._show_index:
return Response(
bdraco marked this conversation as resolved.
Show resolved Hide resolved
text=self._directory_as_html(filepath), content_type="text/html"
text=self._directory_as_html(file_path),
content_type="text/html",
)
except PermissionError:
else:
raise HTTPForbidden()
else:
raise HTTPForbidden()
elif filepath.is_file():
return FileResponse(filepath, chunk_size=self._chunk_size)
else:
raise HTTPNotFound
except PermissionError as error:
raise HTTPForbidden() from error

Check warning on line 685 in aiohttp/web_urldispatcher.py

View check run for this annotation

Codecov / codecov/patch

aiohttp/web_urldispatcher.py#L685

Added line #L685 was not covered by tests
steverep marked this conversation as resolved.
Show resolved Hide resolved

# Not a regular file or does not exist.
if not file_path.is_file():
raise HTTPNotFound()

def _directory_as_html(self, filepath: Path) -> str:
# returns directory's index as html
return FileResponse(file_path, chunk_size=self._chunk_size)
bdraco marked this conversation as resolved.
Show resolved Hide resolved

# sanity check
assert filepath.is_dir()
def _directory_as_html(self, dir_path: Path) -> str:
"""returns directory's index as html."""
assert dir_path.is_dir()

relative_path_to_dir = filepath.relative_to(self._directory).as_posix()
relative_path_to_dir = dir_path.relative_to(self._directory).as_posix()
index_of = f"Index of /{html_escape(relative_path_to_dir)}"
h1 = f"<h1>{index_of}</h1>"

index_list = []
dir_index = filepath.iterdir()
dir_index = dir_path.iterdir()
for _file in sorted(dir_index):
# show file url as relative to static path
rel_path = _file.relative_to(self._directory).as_posix()
Expand Down
Loading