Compare commits
	
		
			10 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 0b750593da | ||
|   | 5b1686ceba | ||
|   | 86baaef1ec | ||
|   | 2b4b78da88 | ||
|   | ee6d8cfe11 | ||
|   | c4da66bf1f | ||
|   | d50d3b8448 | ||
|   | 313f97ac77 | ||
|   | a23547d73b | ||
|   | 34d1dee407 | 
							
								
								
									
										8
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							| @@ -2,9 +2,13 @@ name: "CodeQL" | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [ main ] | ||||
|     branches: | ||||
|       - main | ||||
|       - "*LTS" | ||||
|   pull_request: | ||||
|     branches: [ main ] | ||||
|     branches: | ||||
|       - main | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|   schedule: | ||||
|     - cron: '25 16 * * 0' | ||||
|   | ||||
							
								
								
									
										1
									
								
								.github/workflows/coverage.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/coverage.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | ||||
|   push: | ||||
|     branches: | ||||
|       - main | ||||
|       - "*LTS" | ||||
|     tags: | ||||
|       - "!*" # Do not execute on tags | ||||
|   pull_request: | ||||
|   | ||||
							
								
								
									
										1
									
								
								.github/workflows/pr-bandit.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-bandit.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|  | ||||
| jobs: | ||||
|   | ||||
							
								
								
									
										1
									
								
								.github/workflows/pr-docs.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-docs.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|  | ||||
| jobs: | ||||
|   | ||||
							
								
								
									
										1
									
								
								.github/workflows/pr-linter.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-linter.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|  | ||||
| jobs: | ||||
|   | ||||
							
								
								
									
										1
									
								
								.github/workflows/pr-python310.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-python310.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|  | ||||
| jobs: | ||||
|   | ||||
							
								
								
									
										1
									
								
								.github/workflows/pr-python37.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-python37.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|  | ||||
| jobs: | ||||
|   | ||||
							
								
								
									
										1
									
								
								.github/workflows/pr-python38.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-python38.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|  | ||||
| jobs: | ||||
|   | ||||
							
								
								
									
										1
									
								
								.github/workflows/pr-python39.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-python39.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|  | ||||
| jobs: | ||||
|   | ||||
							
								
								
									
										3
									
								
								.github/workflows/pr-type-check.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/workflows/pr-type-check.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|  | ||||
| jobs: | ||||
| @@ -15,7 +16,7 @@ jobs: | ||||
|       matrix: | ||||
|         os: [ubuntu-latest] | ||||
|         config: | ||||
|           - { python-version: 3.7, tox-env: type-checking} | ||||
|           # - { python-version: 3.7, tox-env: type-checking} | ||||
|           - { python-version: 3.8, tox-env: type-checking} | ||||
|           - { python-version: 3.9, tox-env: type-checking} | ||||
|           - { python-version: "3.10", tox-env: type-checking} | ||||
|   | ||||
							
								
								
									
										1
									
								
								.github/workflows/pr-windows.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/pr-windows.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,6 +3,7 @@ on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - main | ||||
|       - "*LTS" | ||||
|     types: [opened, synchronize, reopened, ready_for_review] | ||||
|  | ||||
| jobs: | ||||
|   | ||||
							
								
								
									
										28
									
								
								codecov.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								codecov.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| coverage: | ||||
|   status: | ||||
|     patch: | ||||
|       default: | ||||
|         target: auto | ||||
|         threshold: 0.75 | ||||
|         informational: true | ||||
|     project: | ||||
|       default: | ||||
|         target: auto | ||||
|         threshold: 0.5 | ||||
|   precision: 3 | ||||
| codecov: | ||||
|   require_ci_to_pass: false | ||||
| ignore: | ||||
|   - "sanic/__main__.py" | ||||
|   - "sanic/compat.py" | ||||
|   - "sanic/reloader_helpers.py" | ||||
|   - "sanic/simple.py" | ||||
|   - "sanic/utils.py" | ||||
|   - "sanic/cli" | ||||
|   - ".github/" | ||||
|   - "changelogs/" | ||||
|   - "docker/" | ||||
|   - "docs/" | ||||
|   - "examples/" | ||||
|   - "scripts/" | ||||
|   - "tests/" | ||||
| @@ -1,3 +1,9 @@ | ||||
| ## Version 21.12.1 | ||||
|  | ||||
| - [#2349](https://github.com/sanic-org/sanic/pull/2349) Only display MOTD on startup | ||||
| - [#2354](https://github.com/sanic-org/sanic/pull/2354) Ignore name argument in Python 3.7 | ||||
| - [#2355](https://github.com/sanic-org/sanic/pull/2355) Add config.update support for all config values | ||||
|  | ||||
| ## Version 21.12.0 | ||||
|  | ||||
| ### Features | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| __version__ = "21.12.0" | ||||
| __version__ = "21.12.2" | ||||
|   | ||||
							
								
								
									
										58
									
								
								sanic/app.py
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								sanic/app.py
									
									
									
									
									
								
							| @@ -1552,10 +1552,19 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|         name: Optional[str] = None, | ||||
|         register: bool = True, | ||||
|     ) -> Task: | ||||
|         prepped = cls._prep_task(task, app, loop) | ||||
|         task = loop.create_task(prepped, name=name) | ||||
|         if not isinstance(task, Future): | ||||
|             prepped = cls._prep_task(task, app, loop) | ||||
|             if sys.version_info < (3, 8): | ||||
|                 if name: | ||||
|                     error_logger.warning( | ||||
|                         "Cannot set a name for a task when using Python 3.7. " | ||||
|                         "Your task will be created without a name." | ||||
|                     ) | ||||
|                 task = loop.create_task(prepped) | ||||
|             else: | ||||
|                 task = loop.create_task(prepped, name=name) | ||||
|  | ||||
|         if name and register: | ||||
|         if name and register and sys.version_info > (3, 7): | ||||
|             app._task_registry[name] = task | ||||
|  | ||||
|         return task | ||||
| @@ -1617,10 +1626,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|     def get_task( | ||||
|         self, name: str, *, raise_exception: bool = True | ||||
|     ) -> Optional[Task]: | ||||
|         if sys.version_info == (3, 7): | ||||
|             raise RuntimeError( | ||||
|                 "This feature is only supported on using Python 3.8+." | ||||
|         if sys.version_info < (3, 8): | ||||
|             error_logger.warning( | ||||
|                 "This feature (get_task) is only supported on using " | ||||
|                 "Python 3.8+." | ||||
|             ) | ||||
|             return | ||||
|         try: | ||||
|             return self._task_registry[name] | ||||
|         except KeyError: | ||||
| @@ -1637,10 +1648,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|         *, | ||||
|         raise_exception: bool = True, | ||||
|     ) -> None: | ||||
|         if sys.version_info == (3, 7): | ||||
|             raise RuntimeError( | ||||
|                 "This feature is only supported on using Python 3.8+." | ||||
|         if sys.version_info < (3, 8): | ||||
|             error_logger.warning( | ||||
|                 "This feature (cancel_task) is only supported on using " | ||||
|                 "Python 3.8+." | ||||
|             ) | ||||
|             return | ||||
|         task = self.get_task(name, raise_exception=raise_exception) | ||||
|         if task and not task.cancelled(): | ||||
|             args: Tuple[str, ...] = () | ||||
| @@ -1659,10 +1672,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|                 ... | ||||
|  | ||||
|     def purge_tasks(self): | ||||
|         if sys.version_info == (3, 7): | ||||
|             raise RuntimeError( | ||||
|                 "This feature is only supported on using Python 3.8+." | ||||
|         if sys.version_info < (3, 8): | ||||
|             error_logger.warning( | ||||
|                 "This feature (purge_tasks) is only supported on using " | ||||
|                 "Python 3.8+." | ||||
|             ) | ||||
|             return | ||||
|         for task in self.tasks: | ||||
|             if task.done() or task.cancelled(): | ||||
|                 name = task.get_name() | ||||
| @@ -1675,10 +1690,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|     def shutdown_tasks( | ||||
|         self, timeout: Optional[float] = None, increment: float = 0.1 | ||||
|     ): | ||||
|         if sys.version_info == (3, 7): | ||||
|             raise RuntimeError( | ||||
|                 "This feature is only supported on using Python 3.8+." | ||||
|         if sys.version_info < (3, 8): | ||||
|             error_logger.warning( | ||||
|                 "This feature (shutdown_tasks) is only supported on using " | ||||
|                 "Python 3.8+." | ||||
|             ) | ||||
|             return | ||||
|         for task in self.tasks: | ||||
|             task.cancel() | ||||
|  | ||||
| @@ -1692,10 +1709,12 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|  | ||||
|     @property | ||||
|     def tasks(self): | ||||
|         if sys.version_info == (3, 7): | ||||
|             raise RuntimeError( | ||||
|                 "This feature is only supported on using Python 3.8+." | ||||
|         if sys.version_info < (3, 8): | ||||
|             error_logger.warning( | ||||
|                 "This feature (tasks) is only supported on using " | ||||
|                 "Python 3.8+." | ||||
|             ) | ||||
|             return | ||||
|         return iter(self._task_registry.values()) | ||||
|  | ||||
|     # -------------------------------------------------------------------- # | ||||
| @@ -1709,7 +1728,8 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta): | ||||
|         details: https://asgi.readthedocs.io/en/latest | ||||
|         """ | ||||
|         self.asgi = True | ||||
|         self.motd("") | ||||
|         if scope["type"] == "lifespan": | ||||
|             self.motd("") | ||||
|         self._asgi_app = await ASGIApp.create(self, scope, receive, send) | ||||
|         asgi_app = self._asgi_app | ||||
|         await asgi_app() | ||||
|   | ||||
| @@ -124,22 +124,27 @@ class Config(dict, metaclass=DescriptorMeta): | ||||
|             raise AttributeError(f"Config has no '{ke.args[0]}'") | ||||
|  | ||||
|     def __setattr__(self, attr, value) -> None: | ||||
|         if attr in self.__class__.__setters__: | ||||
|             try: | ||||
|                 super().__setattr__(attr, value) | ||||
|             except AttributeError: | ||||
|                 ... | ||||
|             else: | ||||
|                 return None | ||||
|         self.update({attr: value}) | ||||
|  | ||||
|     def __setitem__(self, attr, value) -> None: | ||||
|         self.update({attr: value}) | ||||
|  | ||||
|     def update(self, *other, **kwargs) -> None: | ||||
|         other_mapping = {k: v for item in other for k, v in dict(item).items()} | ||||
|         super().update(*other, **kwargs) | ||||
|         for attr, value in {**other_mapping, **kwargs}.items(): | ||||
|         kwargs.update({k: v for item in other for k, v in dict(item).items()}) | ||||
|         setters: Dict[str, Any] = { | ||||
|             k: kwargs.pop(k) | ||||
|             for k in {**kwargs}.keys() | ||||
|             if k in self.__class__.__setters__ | ||||
|         } | ||||
|  | ||||
|         for key, value in setters.items(): | ||||
|             try: | ||||
|                 super().__setattr__(key, value) | ||||
|             except AttributeError: | ||||
|                 ... | ||||
|  | ||||
|         super().update(**kwargs) | ||||
|         for attr, value in {**setters, **kwargs}.items(): | ||||
|             self._post_set(attr, value) | ||||
|  | ||||
|     def _post_set(self, attr, value) -> None: | ||||
|   | ||||
| @@ -4,8 +4,7 @@ from functools import partial, wraps | ||||
| from inspect import getsource, signature | ||||
| from mimetypes import guess_type | ||||
| from os import path | ||||
| from pathlib import PurePath | ||||
| from re import sub | ||||
| from pathlib import Path, PurePath | ||||
| from textwrap import dedent | ||||
| from time import gmtime, strftime | ||||
| from typing import Any, Callable, Iterable, List, Optional, Set, Tuple, Union | ||||
| @@ -17,12 +16,7 @@ from sanic.base.meta import SanicMeta | ||||
| from sanic.compat import stat_async | ||||
| from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS | ||||
| from sanic.errorpages import RESPONSE_MAPPING | ||||
| from sanic.exceptions import ( | ||||
|     ContentRangeError, | ||||
|     FileNotFound, | ||||
|     HeaderNotFound, | ||||
|     InvalidUsage, | ||||
| ) | ||||
| from sanic.exceptions import ContentRangeError, FileNotFound, HeaderNotFound | ||||
| from sanic.handlers import ContentRangeHandler | ||||
| from sanic.log import deprecation, error_logger | ||||
| from sanic.models.futures import FutureRoute, FutureStatic | ||||
| @@ -775,32 +769,40 @@ class RouteMixin(metaclass=SanicMeta): | ||||
|         content_type=None, | ||||
|         __file_uri__=None, | ||||
|     ): | ||||
|         # Using this to determine if the URL is trying to break out of the path | ||||
|         # served.  os.path.realpath seems to be very slow | ||||
|         if __file_uri__ and "../" in __file_uri__: | ||||
|             raise InvalidUsage("Invalid URL") | ||||
|         # Merge served directory and requested file if provided | ||||
|         # Strip all / that in the beginning of the URL to help prevent python | ||||
|         # from herping a derp and treating the uri as an absolute path | ||||
|         root_path = file_path = file_or_directory | ||||
|         if __file_uri__: | ||||
|             file_path = path.join( | ||||
|                 file_or_directory, sub("^[/]*", "", __file_uri__) | ||||
|             ) | ||||
|         file_path_raw = Path(unquote(file_or_directory)) | ||||
|         root_path = file_path = file_path_raw.resolve() | ||||
|         not_found = FileNotFound( | ||||
|             "File not found", | ||||
|             path=file_or_directory, | ||||
|             relative_url=__file_uri__, | ||||
|         ) | ||||
|  | ||||
|         # URL decode the path sent by the browser otherwise we won't be able to | ||||
|         # match filenames which got encoded (filenames with spaces etc) | ||||
|         file_path = path.abspath(unquote(file_path)) | ||||
|         if not file_path.startswith(path.abspath(unquote(root_path))): | ||||
|             error_logger.exception( | ||||
|                 f"File not found: path={file_or_directory}, " | ||||
|                 f"relative_url={__file_uri__}" | ||||
|             ) | ||||
|             raise FileNotFound( | ||||
|                 "File not found", | ||||
|                 path=file_or_directory, | ||||
|                 relative_url=__file_uri__, | ||||
|             ) | ||||
|         if __file_uri__: | ||||
|             # Strip all / that in the beginning of the URL to help prevent | ||||
|             # python from herping a derp and treating the uri as an | ||||
|             # absolute path | ||||
|             unquoted_file_uri = unquote(__file_uri__).lstrip("/") | ||||
|             file_path_raw = Path(file_or_directory, unquoted_file_uri) | ||||
|             file_path = file_path_raw.resolve() | ||||
|             if ( | ||||
|                 file_path < root_path and not file_path_raw.is_symlink() | ||||
|             ) or ".." in file_path_raw.parts: | ||||
|                 error_logger.exception( | ||||
|                     f"File not found: path={file_or_directory}, " | ||||
|                     f"relative_url={__file_uri__}" | ||||
|                 ) | ||||
|                 raise not_found | ||||
|  | ||||
|         try: | ||||
|             file_path.relative_to(root_path) | ||||
|         except ValueError: | ||||
|             if not file_path_raw.is_symlink(): | ||||
|                 error_logger.exception( | ||||
|                     f"File not found: path={file_or_directory}, " | ||||
|                     f"relative_url={__file_uri__}" | ||||
|                 ) | ||||
|                 raise not_found | ||||
|         try: | ||||
|             headers = {} | ||||
|             # Check if the client has been sent this file before | ||||
| @@ -868,11 +870,7 @@ class RouteMixin(metaclass=SanicMeta): | ||||
|         except ContentRangeError: | ||||
|             raise | ||||
|         except FileNotFoundError: | ||||
|             raise FileNotFound( | ||||
|                 "File not found", | ||||
|                 path=file_or_directory, | ||||
|                 relative_url=__file_uri__, | ||||
|             ) | ||||
|             raise not_found | ||||
|         except Exception: | ||||
|             error_logger.exception( | ||||
|                 f"Exception in static request handler: " | ||||
|   | ||||
| @@ -5,7 +5,7 @@ from os import environ | ||||
| from pathlib import Path | ||||
| from tempfile import TemporaryDirectory | ||||
| from textwrap import dedent | ||||
| from unittest.mock import Mock | ||||
| from unittest.mock import Mock, call | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| @@ -385,5 +385,24 @@ def test_config_set_methods(app, monkeypatch): | ||||
|     post_set.assert_called_once_with("FOO", 5) | ||||
|     post_set.reset_mock() | ||||
|  | ||||
|     app.config.update_config({"FOO": 6}) | ||||
|     post_set.assert_called_once_with("FOO", 6) | ||||
|     app.config.update({"FOO": 6}, {"BAR": 7}) | ||||
|     post_set.assert_has_calls( | ||||
|         calls=[ | ||||
|             call("FOO", 6), | ||||
|             call("BAR", 7), | ||||
|         ] | ||||
|     ) | ||||
|     post_set.reset_mock() | ||||
|  | ||||
|     app.config.update({"FOO": 8}, BAR=9) | ||||
|     post_set.assert_has_calls( | ||||
|         calls=[ | ||||
|             call("FOO", 8), | ||||
|             call("BAR", 9), | ||||
|         ], | ||||
|         any_order=True, | ||||
|     ) | ||||
|     post_set.reset_mock() | ||||
|  | ||||
|     app.config.update_config({"FOO": 10}) | ||||
|     post_set.assert_called_once_with("FOO", 10) | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import asyncio | ||||
| import sys | ||||
|  | ||||
| from threading import Event | ||||
| from unittest.mock import Mock | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| @@ -77,6 +78,25 @@ def test_create_named_task(app): | ||||
|     app.run() | ||||
|  | ||||
|  | ||||
| def test_named_task_called(app): | ||||
|     e = Event() | ||||
|  | ||||
|     async def coro(): | ||||
|         e.set() | ||||
|  | ||||
|     @app.route("/") | ||||
|     async def isset(request): | ||||
|         await asyncio.sleep(0.05) | ||||
|         return text(str(e.is_set())) | ||||
|  | ||||
|     @app.before_server_start | ||||
|     async def setup(app, _): | ||||
|         app.add_task(coro, name="dummy_task") | ||||
|  | ||||
|     request, response = app.test_client.get("/") | ||||
|     assert response.body == b"True" | ||||
|  | ||||
|  | ||||
| @pytest.mark.skipif(sys.version_info < (3, 8), reason="Not supported in 3.7") | ||||
| def test_create_named_task_fails_outside_app(app): | ||||
|     async def dummy(): | ||||
|   | ||||
| @@ -334,6 +334,22 @@ def test_config_fallback_before_and_after_startup(app): | ||||
|     assert response.content_type == "text/plain; charset=utf-8" | ||||
|  | ||||
|  | ||||
| def test_config_fallback_using_update_dict(app): | ||||
|     app.config.update({"FALLBACK_ERROR_FORMAT": "text"}) | ||||
|  | ||||
|     _, response = app.test_client.get("/error") | ||||
|     assert response.status == 500 | ||||
|     assert response.content_type == "text/plain; charset=utf-8" | ||||
|  | ||||
|  | ||||
| def test_config_fallback_using_update_kwarg(app): | ||||
|     app.config.update(FALLBACK_ERROR_FORMAT="text") | ||||
|  | ||||
|     _, response = app.test_client.get("/error") | ||||
|     assert response.status == 500 | ||||
|     assert response.content_type == "text/plain; charset=utf-8" | ||||
|  | ||||
|  | ||||
| def test_config_fallback_bad_value(app): | ||||
|     message = "Unknown format: fake" | ||||
|     with pytest.raises(SanicException, match=message): | ||||
|   | ||||
| @@ -62,19 +62,15 @@ def test_streaming_body_requests(app): | ||||
|  | ||||
|     data = ["hello", "world"] | ||||
|  | ||||
|     class Data(AsyncByteStream): | ||||
|         def __init__(self, data): | ||||
|             self.data = data | ||||
|  | ||||
|         async def __aiter__(self): | ||||
|             for value in self.data: | ||||
|                 yield value.encode("utf-8") | ||||
|  | ||||
|     client = ReusableClient(app, port=1234) | ||||
|  | ||||
|     async def stream(data): | ||||
|         for value in data: | ||||
|             yield value.encode("utf-8") | ||||
|  | ||||
|     with client: | ||||
|         _, response1 = client.post("/", data=Data(data)) | ||||
|         _, response2 = client.post("/", data=Data(data)) | ||||
|         _, response1 = client.post("/", data=stream(data)) | ||||
|         _, response2 = client.post("/", data=stream(data)) | ||||
|  | ||||
|     assert response1.status == response2.status == 200 | ||||
|     assert response1.json["data"] == response2.json["data"] == data | ||||
|   | ||||
| @@ -4,8 +4,8 @@ from unittest.mock import Mock, patch | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from sanic.server import loop | ||||
| from sanic.compat import OS_IS_WINDOWS, UVLOOP_INSTALLED | ||||
| from sanic.server import loop | ||||
|  | ||||
|  | ||||
| @pytest.mark.skipif( | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import inspect | ||||
| import logging | ||||
| import os | ||||
| import sys | ||||
|  | ||||
| from collections import Counter | ||||
| from pathlib import Path | ||||
| @@ -8,7 +9,7 @@ from time import gmtime, strftime | ||||
|  | ||||
| import pytest | ||||
|  | ||||
| from sanic import text | ||||
| from sanic import Sanic, text | ||||
| from sanic.exceptions import FileNotFound | ||||
|  | ||||
|  | ||||
| @@ -21,6 +22,22 @@ def static_file_directory(): | ||||
|     return static_directory | ||||
|  | ||||
|  | ||||
| @pytest.fixture(scope="module") | ||||
| def double_dotted_directory_file(static_file_directory: str): | ||||
|     """Generate double dotted directory and its files""" | ||||
|     if sys.platform == "win32": | ||||
|         raise Exception("Windows doesn't support double dotted directories") | ||||
|  | ||||
|     file_path = Path(static_file_directory) / "dotted.." / "dot.txt" | ||||
|     double_dotted_dir = file_path.parent | ||||
|     Path.mkdir(double_dotted_dir, exist_ok=True) | ||||
|     with open(file_path, "w") as f: | ||||
|         f.write("DOT\n") | ||||
|     yield file_path | ||||
|     Path.unlink(file_path) | ||||
|     Path.rmdir(double_dotted_dir) | ||||
|  | ||||
|  | ||||
| def get_file_path(static_file_directory, file_name): | ||||
|     return os.path.join(static_file_directory, file_name) | ||||
|  | ||||
| @@ -578,3 +595,43 @@ def test_resource_type_dir(app, static_file_directory): | ||||
| def test_resource_type_unknown(app, static_file_directory, caplog): | ||||
|     with pytest.raises(ValueError): | ||||
|         app.static("/static", static_file_directory, resource_type="unknown") | ||||
|  | ||||
|  | ||||
| @pytest.mark.skipif( | ||||
|     sys.platform == "win32", | ||||
|     reason="Windows does not support double dotted directories", | ||||
| ) | ||||
| def test_dotted_dir_ok( | ||||
|     app: Sanic, static_file_directory: str, double_dotted_directory_file: Path | ||||
| ): | ||||
|     app.static("/foo", static_file_directory) | ||||
|     dot_relative_path = str( | ||||
|         double_dotted_directory_file.relative_to(static_file_directory) | ||||
|     ) | ||||
|     _, response = app.test_client.get("/foo/" + dot_relative_path) | ||||
|     assert response.status == 200 | ||||
|     assert response.body == b"DOT\n" | ||||
|  | ||||
|  | ||||
| def test_breakout(app: Sanic, static_file_directory: str): | ||||
|     app.static("/foo", static_file_directory) | ||||
|  | ||||
|     _, response = app.test_client.get("/foo/..%2Ffake/server.py") | ||||
|     assert response.status == 404 | ||||
|  | ||||
|     _, response = app.test_client.get("/foo/..%2Fstatic/test.file") | ||||
|     assert response.status == 404 | ||||
|  | ||||
|  | ||||
| @pytest.mark.skipif( | ||||
|     sys.platform != "win32", reason="Block backslash on Windows only" | ||||
| ) | ||||
| def test_double_backslash_prohibited_on_win32( | ||||
|     app: Sanic, static_file_directory: str | ||||
| ): | ||||
|     app.static("/foo", static_file_directory) | ||||
|  | ||||
|     _, response = app.test_client.get("/foo/static/..\\static/test.file") | ||||
|     assert response.status == 404 | ||||
|     _, response = app.test_client.get("/foo/static\\../static/test.file") | ||||
|     assert response.status == 404 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user