diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index 2560c42e89e..be03975a145 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -9,6 +9,7 @@ import re from urllib.parse import quote, urlencode from collections import namedtuple +from pathlib import Path from . import hdrs, multidict from .errors import InvalidURL @@ -204,7 +205,7 @@ def str_to_bytes(s, encoding='utf-8'): def guess_filename(obj, default=None): name = getattr(obj, 'name', None) if name and name[0] != '<' and name[-1] != '>': - return os.path.split(name)[-1] + return Path(name).name return default diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py index 32f420c882f..f5a9e95f6fb 100644 --- a/aiohttp/multipart.py +++ b/aiohttp/multipart.py @@ -11,6 +11,7 @@ import zlib from urllib.parse import quote, unquote, urlencode, parse_qsl from collections import deque, Mapping, Sequence +from pathlib import Path from .helpers import parse_mimetype from .multidict import CIMultiDict @@ -652,7 +653,7 @@ def _guess_filename(self, obj): if isinstance(obj, io.IOBase): name = getattr(obj, 'name', None) if name is not None: - return os.path.basename(name) + return Path(name).name def serialize(self): """Yields byte chunks for body part.""" diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py index 3bfcaaac50d..bff06ad0074 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -10,6 +10,7 @@ import inspect from collections.abc import Sized, Iterable, Container +from pathlib import Path from urllib.parse import urlencode, unquote from types import MappingProxyType @@ -159,14 +160,17 @@ def __init__(self, name, prefix, directory, *, 'GET', self.handle, name, expect_handler=expect_handler) self._prefix = prefix self._prefix_len = len(self._prefix) - self._directory = os.path.abspath(directory) + os.sep + try: + directory = Path(directory).resolve() + if not directory.is_dir(): + raise ValueError('Not a directory') + except (FileNotFoundError, ValueError) as error: + raise ValueError( + "No directory exists at '{}'".format(directory)) from error + self._directory = directory self._chunk_size = chunk_size self._response_factory = response_factory - if not os.path.isdir(self._directory): - raise ValueError( - "No directory exists at '{}'".format(self._directory)) - if bool(os.environ.get("AIOHTTP_NOSENDFILE")): self._sendfile = self._sendfile_fallback @@ -176,6 +180,8 @@ def match(self, path): return {'filename': path[self._prefix_len:]} def url(self, *, filename, query=None): + if isinstance(filename, Path): + filename = str(getattr(filename, 'path', filename)) while filename.startswith('/'): filename = filename[1:] url = self._prefix + filename @@ -266,19 +272,25 @@ def _sendfile_fallback(self, req, resp, fobj, count): @asyncio.coroutine def handle(self, request): filename = request.match_info['filename'] - filepath = os.path.abspath(os.path.join(self._directory, filename)) - if not filepath.startswith(self._directory): - raise HTTPNotFound() - if not os.path.exists(filepath) or not os.path.isfile(filepath): - raise HTTPNotFound() - - st = os.stat(filepath) + try: + filepath = self._directory.joinpath(filename).resolve() + filepath.relative_to(self._directory) + except (ValueError, FileNotFoundError) as error: + # relatively safe + raise HTTPNotFound() from error + except Exception as error: + # perm error or other kind! + request.logger.exception(error) + raise HTTPNotFound() from error + + st = filepath.stat() modsince = request.if_modified_since if modsince is not None and st.st_mtime <= modsince.timestamp(): raise HTTPNotModified() - ct, encoding = mimetypes.guess_type(filepath) + path = str(getattr(filepath, 'path', filename)) + ct, encoding = mimetypes.guess_type(path) if not ct: ct = 'application/octet-stream' @@ -295,7 +307,7 @@ def handle(self, request): try: yield from resp.prepare(request) - with open(filepath, 'rb') as f: + with filepath.open('rb') as f: yield from self._sendfile(request, resp, f, file_size) finally: