Compare commits
5 Commits
py37-catch
...
21.12LTS
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b750593da | ||
|
|
5b1686ceba | ||
|
|
86baaef1ec | ||
|
|
2b4b78da88 | ||
|
|
ee6d8cfe11 |
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 +1 @@
|
|||||||
__version__ = "21.12.1"
|
__version__ = "21.12.2"
|
||||||
|
|||||||
@@ -1004,10 +1004,10 @@ class Sanic(BaseSanic, metaclass=TouchUpMeta):
|
|||||||
cancelled = False
|
cancelled = False
|
||||||
try:
|
try:
|
||||||
await fut
|
await fut
|
||||||
except (CancelledError, ConnectionClosed):
|
|
||||||
cancelled = True
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.error_handler.log(request, e)
|
self.error_handler.log(request, e)
|
||||||
|
except (CancelledError, ConnectionClosed):
|
||||||
|
cancelled = True
|
||||||
finally:
|
finally:
|
||||||
self.websocket_tasks.remove(fut)
|
self.websocket_tasks.remove(fut)
|
||||||
if cancelled:
|
if cancelled:
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ DEFAULT_CONFIG = {
|
|||||||
"REQUEST_TIMEOUT": 60, # 60 seconds
|
"REQUEST_TIMEOUT": 60, # 60 seconds
|
||||||
"RESPONSE_TIMEOUT": 60, # 60 seconds
|
"RESPONSE_TIMEOUT": 60, # 60 seconds
|
||||||
"USE_UVLOOP": _default,
|
"USE_UVLOOP": _default,
|
||||||
"WEBSOCKET_MAX_SIZE": 2**20, # 1 megabyte
|
"WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabyte
|
||||||
"WEBSOCKET_PING_INTERVAL": 20,
|
"WEBSOCKET_PING_INTERVAL": 20,
|
||||||
"WEBSOCKET_PING_TIMEOUT": 20,
|
"WEBSOCKET_PING_TIMEOUT": 20,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ Options = Dict[str, Union[int, str]] # key=value fields in various headers
|
|||||||
OptionsIterable = Iterable[Tuple[str, str]] # May contain duplicate keys
|
OptionsIterable = Iterable[Tuple[str, str]] # May contain duplicate keys
|
||||||
|
|
||||||
_token, _quoted = r"([\w!#$%&'*+\-.^_`|~]+)", r'"([^"]*)"'
|
_token, _quoted = r"([\w!#$%&'*+\-.^_`|~]+)", r'"([^"]*)"'
|
||||||
_param = re.compile(rf";\s*{_token}=(?:{_token}|{_quoted})", re.ASCII)
|
_param = re.compile(fr";\s*{_token}=(?:{_token}|{_quoted})", re.ASCII)
|
||||||
_firefox_quote_escape = re.compile(r'\\"(?!; |\s*$)')
|
_firefox_quote_escape = re.compile(r'\\"(?!; |\s*$)')
|
||||||
_ipv6 = "(?:[0-9A-Fa-f]{0,4}:){2,7}[0-9A-Fa-f]{0,4}"
|
_ipv6 = "(?:[0-9A-Fa-f]{0,4}:){2,7}[0-9A-Fa-f]{0,4}"
|
||||||
_ipv6_re = re.compile(_ipv6)
|
_ipv6_re = re.compile(_ipv6)
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ from functools import partial, wraps
|
|||||||
from inspect import getsource, signature
|
from inspect import getsource, signature
|
||||||
from mimetypes import guess_type
|
from mimetypes import guess_type
|
||||||
from os import path
|
from os import path
|
||||||
from pathlib import PurePath
|
from pathlib import Path, PurePath
|
||||||
from re import sub
|
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
from time import gmtime, strftime
|
from time import gmtime, strftime
|
||||||
from typing import Any, Callable, Iterable, List, Optional, Set, Tuple, Union
|
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.compat import stat_async
|
||||||
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS
|
from sanic.constants import DEFAULT_HTTP_CONTENT_TYPE, HTTP_METHODS
|
||||||
from sanic.errorpages import RESPONSE_MAPPING
|
from sanic.errorpages import RESPONSE_MAPPING
|
||||||
from sanic.exceptions import (
|
from sanic.exceptions import ContentRangeError, FileNotFound, HeaderNotFound
|
||||||
ContentRangeError,
|
|
||||||
FileNotFound,
|
|
||||||
HeaderNotFound,
|
|
||||||
InvalidUsage,
|
|
||||||
)
|
|
||||||
from sanic.handlers import ContentRangeHandler
|
from sanic.handlers import ContentRangeHandler
|
||||||
from sanic.log import deprecation, error_logger
|
from sanic.log import deprecation, error_logger
|
||||||
from sanic.models.futures import FutureRoute, FutureStatic
|
from sanic.models.futures import FutureRoute, FutureStatic
|
||||||
@@ -775,32 +769,40 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
content_type=None,
|
content_type=None,
|
||||||
__file_uri__=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
|
# Merge served directory and requested file if provided
|
||||||
# Strip all / that in the beginning of the URL to help prevent python
|
file_path_raw = Path(unquote(file_or_directory))
|
||||||
# from herping a derp and treating the uri as an absolute path
|
root_path = file_path = file_path_raw.resolve()
|
||||||
root_path = file_path = file_or_directory
|
not_found = FileNotFound(
|
||||||
if __file_uri__:
|
"File not found",
|
||||||
file_path = path.join(
|
path=file_or_directory,
|
||||||
file_or_directory, sub("^[/]*", "", __file_uri__)
|
relative_url=__file_uri__,
|
||||||
)
|
)
|
||||||
|
|
||||||
# URL decode the path sent by the browser otherwise we won't be able to
|
if __file_uri__:
|
||||||
# match filenames which got encoded (filenames with spaces etc)
|
# Strip all / that in the beginning of the URL to help prevent
|
||||||
file_path = path.abspath(unquote(file_path))
|
# python from herping a derp and treating the uri as an
|
||||||
if not file_path.startswith(path.abspath(unquote(root_path))):
|
# absolute path
|
||||||
error_logger.exception(
|
unquoted_file_uri = unquote(__file_uri__).lstrip("/")
|
||||||
f"File not found: path={file_or_directory}, "
|
file_path_raw = Path(file_or_directory, unquoted_file_uri)
|
||||||
f"relative_url={__file_uri__}"
|
file_path = file_path_raw.resolve()
|
||||||
)
|
if (
|
||||||
raise FileNotFound(
|
file_path < root_path and not file_path_raw.is_symlink()
|
||||||
"File not found",
|
) or ".." in file_path_raw.parts:
|
||||||
path=file_or_directory,
|
error_logger.exception(
|
||||||
relative_url=__file_uri__,
|
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:
|
try:
|
||||||
headers = {}
|
headers = {}
|
||||||
# Check if the client has been sent this file before
|
# Check if the client has been sent this file before
|
||||||
@@ -868,11 +870,7 @@ class RouteMixin(metaclass=SanicMeta):
|
|||||||
except ContentRangeError:
|
except ContentRangeError:
|
||||||
raise
|
raise
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise FileNotFound(
|
raise not_found
|
||||||
"File not found",
|
|
||||||
path=file_or_directory,
|
|
||||||
relative_url=__file_uri__,
|
|
||||||
)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
error_logger.exception(
|
error_logger.exception(
|
||||||
f"Exception in static request handler: "
|
f"Exception in static request handler: "
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -8,7 +9,7 @@ from time import gmtime, strftime
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sanic import text
|
from sanic import Sanic, text
|
||||||
from sanic.exceptions import FileNotFound
|
from sanic.exceptions import FileNotFound
|
||||||
|
|
||||||
|
|
||||||
@@ -21,6 +22,22 @@ def static_file_directory():
|
|||||||
return static_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):
|
def get_file_path(static_file_directory, file_name):
|
||||||
return os.path.join(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):
|
def test_resource_type_unknown(app, static_file_directory, caplog):
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
app.static("/static", static_file_directory, resource_type="unknown")
|
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