Compare commits

..

9 Commits

Author SHA1 Message Date
Adam Hopkins
05002d7ee4 Path protection with pathlib 2022-07-31 14:09:01 +03:00
Adam Hopkins
b4360d4a20 Path protection with pathlib 2022-07-31 13:26:11 +03:00
Adam Hopkins
3b85b3bbad Potential server crash if running Python 3.10 w/ Sanic 20.12 (#2400) 2022-02-16 18:03:05 +02:00
Stephen Sadowski
6e55e73da1 fix: websocket dependency for websockets 9.1 security fix (#2366)
Co-authored-by: Adam Hopkins <adam@amhopkins.com>
2022-01-16 20:42:38 +02:00
Adam Hopkins
89d942451f Merge branch 'pr2129' into 20.12LTS 2021-10-03 01:19:53 +03:00
Adam Hopkins
4d6205e6fe Bump version 2021-10-03 01:05:08 +03:00
Thomas Grainger
1684b0b986 remove reference to yanked packages 2021-07-02 10:02:17 +01:00
Thomas Grainger
4f5faa4a3c unpin uvloop 2021-05-04 18:14:14 +01:00
Arthur Goldberg
cbb77b536a fix issue where request.args.pop removed parameters inconsistently (#2112)
Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2021-04-22 12:49:08 +03:00
28 changed files with 676 additions and 56 deletions

40
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: "CodeQL"
on:
push:
branches:
- main
- "*LTS"
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
schedule:
- cron: '25 16 * * 0'
jobs:
analyze:
if: github.event.pull_request.draft == false
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'python' ]
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

37
.github/workflows/coverage.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Coverage check
on:
push:
branches:
- main
- "*LTS"
tags:
- "!*" # Do not execute on tags
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
jobs:
test:
if: github.event.pull_request.draft == false
runs-on: ${{ matrix.os }}
strategy:
matrix:
python-version: [3.9]
os: [ubuntu-latest]
fail-fast: false
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies 🔨
run: |
python -m pip install --upgrade pip
pip install tox
- uses: paambaati/codeclimate-action@v2.5.3
if: always()
env:
CC_TEST_REPORTER_ID: ${{ secrets.CODECLIMATE }}
with:
coverageCommand: tox -e coverage

39
.github/workflows/on-demand.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: On Demand Task
on:
workflow_dispatch:
inputs:
python-version:
description: 'Version of Python to use for running Test'
required: false
default: "3.8"
tox-env:
description: 'Test Environment to Run'
required: true
default: ''
os:
description: 'Operating System to Run Test on'
required: false
default: ubuntu-latest
jobs:
onDemand:
name: tox-${{ matrix.config.tox-env }}-on-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: ["${{ github.event.inputs.os}}"]
config:
- { tox-env: "${{ github.event.inputs.tox-env }}", py-version: "${{ github.event.inputs.python-version }}"}
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Run tests
uses: harshanarayana/custom-actions@main
with:
python-version: ${{ matrix.config.py-version }}
test-infra-tool: tox
test-infra-version: latest
action: tests
test-additional-args: "-e=${{ matrix.config.tox-env }}"
experimental-ignore-error: "yes"

36
.github/workflows/pr-bandit.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
name: Security Analysis
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
bandit:
if: github.event.pull_request.draft == false
name: type-check-${{ matrix.config.python-version }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
config:
- { python-version: 3.7, tox-env: security}
- { python-version: 3.8, tox-env: security}
- { python-version: 3.9, tox-env: security}
- { python-version: "3.10", tox-env: security}
steps:
- name: Checkout the repository
uses: actions/checkout@v2
id: checkout-branch
- name: Run Linter Checks
id: linter-check
uses: harshanarayana/custom-actions@main
with:
python-version: ${{ matrix.config.python-version }}
test-infra-tool: tox
test-infra-version: latest
action: tests
test-additional-args: "-e=${{ matrix.config.tox-env }}"

32
.github/workflows/pr-docs.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: Document Linter
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
docsLinter:
if: github.event.pull_request.draft == false
name: Lint Documentation
runs-on: ubuntu-latest
strategy:
matrix:
config:
- {python-version: "3.8", tox-env: "docs"}
fail-fast: false
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Run Document Linter
uses: harshanarayana/custom-actions@main
with:
python-version: ${{ matrix.config.python-version }}
test-infra-tool: tox
test-infra-version: latest
action: tests
test-additional-args: "-e=${{ matrix.config.tox-env }}"

33
.github/workflows/pr-linter.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: Linter Checks
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
linter:
if: github.event.pull_request.draft == false
name: lint
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
config:
- { python-version: 3.8, tox-env: lint}
steps:
- name: Checkout the repository
uses: actions/checkout@v2
id: checkout-branch
- name: Run Linter Checks
id: linter-check
uses: harshanarayana/custom-actions@main
with:
python-version: ${{ matrix.config.python-version }}
test-infra-tool: tox
test-infra-version: latest
action: tests
test-additional-args: "-e=${{ matrix.config.tox-env }}"

41
.github/workflows/pr-python-pypy.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: Python PyPy Tests
on:
workflow_dispatch:
inputs:
tox-env:
description: "Tox Env to run on the PyPy Infra"
required: false
default: "pypy37"
pypy-version:
description: "Version of PyPy to use"
required: false
default: "pypy-3.7"
jobs:
testPyPy:
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]
config:
- {
python-version: "${{ github.event.inputs.pypy-version }}",
tox-env: "${{ github.event.inputs.tox-env }}",
}
steps:
- name: Checkout the Repository
uses: actions/checkout@v2
id: checkout-branch
- name: Run Unit Tests
uses: harshanarayana/custom-actions@main
with:
python-version: ${{ matrix.config.python-version }}
test-infra-tool: tox
test-infra-version: latest
action: tests
test-additional-args: "-e=${{ matrix.config.tox-env }}"
experimental-ignore-error: "true"
command-timeout: "600000"

35
.github/workflows/pr-python37.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: Python 3.7 Tests
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
testPy37:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]
config:
- { python-version: 3.7, tox-env: py37 }
- { python-version: 3.7, tox-env: py37-no-ext }
steps:
- name: Checkout the Repository
uses: actions/checkout@v2
id: checkout-branch
- name: Run Unit Tests
uses: harshanarayana/custom-actions@main
with:
python-version: ${{ matrix.config.python-version }}
test-infra-tool: tox
test-infra-version: latest
action: tests
test-additional-args: "-e=${{ matrix.config.tox-env }}"
test-failure-retry: "3"

35
.github/workflows/pr-python38.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: Python 3.8 Tests
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
testPy38:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]
config:
- { python-version: 3.8, tox-env: py38 }
- { python-version: 3.8, tox-env: py38-no-ext }
steps:
- name: Checkout the Repository
uses: actions/checkout@v2
id: checkout-branch
- name: Run Unit Tests
uses: harshanarayana/custom-actions@main
with:
python-version: ${{ matrix.config.python-version }}
test-infra-tool: tox
test-infra-version: latest
action: tests
test-additional-args: "-e=${{ matrix.config.tox-env }}"
test-failure-retry: "3"

47
.github/workflows/pr-python39.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: Python 3.9 Tests
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
testPy39:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}-${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: true
matrix:
# os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]
config:
- {
python-version: 3.9,
tox-env: py39,
ignore-error-flake: "false",
command-timeout: "0",
}
- {
python-version: 3.9,
tox-env: py39-no-ext,
ignore-error-flake: "true",
command-timeout: "600000",
}
steps:
- name: Checkout the Repository
uses: actions/checkout@v2
id: checkout-branch
- name: Run Unit Tests
uses: harshanarayana/custom-actions@main
with:
python-version: ${{ matrix.config.python-version }}
test-infra-tool: tox
test-infra-version: latest
action: tests
test-additional-args: "-e=${{ matrix.config.tox-env }},-vv=''"
experimental-ignore-error: "${{ matrix.config.ignore-error-flake }}"
command-timeout: "${{ matrix.config.command-timeout }}"
test-failure-retry: "3"

35
.github/workflows/pr-type-check.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: Typing Checks
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
typeChecking:
if: github.event.pull_request.draft == false
name: type-check-${{ matrix.config.python-version }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
config:
# - { python-version: 3.7, tox-env: type-checking}
- { python-version: 3.8, tox-env: type-checking}
- { python-version: 3.9, tox-env: type-checking}
steps:
- name: Checkout the repository
uses: actions/checkout@v2
id: checkout-branch
- name: Run Linter Checks
id: linter-check
uses: harshanarayana/custom-actions@main
with:
python-version: ${{ matrix.config.python-version }}
test-infra-tool: tox
test-infra-version: latest
action: tests
test-additional-args: "-e=${{ matrix.config.tox-env }}"

37
.github/workflows/pr-windows.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Run Unit Tests on Windows
on:
pull_request:
branches:
- main
- "*LTS"
types: [opened, synchronize, reopened, ready_for_review]
jobs:
testsOnWindows:
if: github.event.pull_request.draft == false
name: ut-${{ matrix.config.tox-env }}
runs-on: windows-latest
strategy:
fail-fast: false
matrix:
config:
- { python-version: 3.7, tox-env: py37-no-ext }
- { python-version: 3.8, tox-env: py38-no-ext }
- { python-version: 3.9, tox-env: py39-no-ext }
- { python-version: pypy-3.7, tox-env: pypy37-no-ext }
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Run Unit Tests
uses: ahopkins/custom-actions@pip-extra-args
with:
python-version: ${{ matrix.config.python-version }}
test-infra-tool: tox
test-infra-version: latest
action: tests
test-additional-args: "-e=${{ matrix.config.tox-env }}"
experimental-ignore-error: "true"
command-timeout: "600000"
pip-extra-args: "--user"

48
.github/workflows/publish-images.yml vendored Normal file
View File

@@ -0,0 +1,48 @@
name: Publish Docker Images
on:
workflow_run:
workflows:
- 'Publish Artifacts'
types:
- completed
jobs:
publishDockerImages:
name: Docker Image Build [${{ matrix.python-version }}]
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
python-version: ["3.7", "3.8", "3.9"]
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Build Latest Base images for ${{ matrix.python-version }}
uses: harshanarayana/custom-actions@main
with:
docker-image-base-name: sanicframework/sanic-build
ignore-python-setup: 'true'
dockerfile-base-dir: './docker'
action: 'image-publish'
docker-image-tag: "${{ matrix.python-version }}"
docker-file-suffix: "base"
docker-build-args: "PYTHON_VERSION=${{ matrix.python-version }}"
registry-auth-user: ${{ secrets.DOCKER_ACCESS_USER }}
registry-auth-password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
push-images: 'true'
- name: Publish Sanic Docker Image for ${{ matrix.python-version }}
uses: harshanarayana/custom-actions@main
with:
docker-image-base-name: sanicframework/sanic
ignore-python-setup: 'true'
dockerfile-base-dir: './docker'
action: 'image-publish'
docker-build-args: "BASE_IMAGE_TAG=${{ matrix.python-version }}"
docker-image-prefix: "${{ matrix.python-version }}"
registry-auth-user: ${{ secrets.DOCKER_ACCESS_USER }}
registry-auth-password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
push-images: 'true'

28
.github/workflows/publish-package.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: Publish Artifacts
on:
release:
types: [created]
jobs:
publishPythonPackage:
name: Publishing Sanic Release Artifacts
runs-on: ubuntu-latest
strategy:
fail-fast: true
matrix:
python-version: ["3.8"]
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Publish Python Package
uses: harshanarayana/custom-actions@main
with:
python-version: ${{ matrix.python-version }}
package-infra-name: "twine"
pypi-user: __token__
pypi-access-token: ${{ secrets.PYPI_ACCESS_TOKEN }}
action: "package-publish"
pypi-verify-metadata: "true"

View File

@@ -1,3 +1,13 @@
Version 20.12.5
===============
Bugfixes
********
*
`#2366 <https://github.com/sanic-org/sanic/pull/2366>`_
websocket dependency for websockets 9.1 security fix
Version 20.12.0 Version 20.12.0
=============== ===============

View File

@@ -1 +1 @@
__version__ = "20.12.3" __version__ = "20.12.7"

View File

@@ -2,6 +2,7 @@ import logging
import logging.config import logging.config
import os import os
import re import re
import sys
from asyncio import CancelledError, Protocol, ensure_future, get_event_loop from asyncio import CancelledError, Protocol, ensure_future, get_event_loop
from collections import defaultdict, deque from collections import defaultdict, deque
@@ -65,6 +66,18 @@ class Sanic:
if configure_logging: if configure_logging:
logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS) logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS)
if sys.version_info >= (3, 10):
error_logger.error(
"Unsupported version of Python has been detected.\n\nPython "
f"version {sys.version} is not supported by this version of "
"Sanic. There is a security advisory that has been issued for "
"Sanic v20.12 while running Python 3.10+. You should either "
"use a supported version of Python (v3.6 - v3.9) or upgrade "
"Sanic to v21+.\n\nPlease see https://github.com/sanic-org/"
"sanic/security/advisories/GHSA-7p79-6x2v-5h88 for "
"more information.\n"
)
self.name = name self.name = name
self.asgi = False self.asgi = False
self.router = router or Router(self) self.router = router or Router(self)

View File

@@ -20,7 +20,6 @@ if use_trio:
def stat_async(path): def stat_async(path):
return Path(path).stat() return Path(path).stat()
else: else:
from aiofiles import open as aio_open # type: ignore from aiofiles import open as aio_open # type: ignore
from aiofiles.os import stat as stat_async # type: ignore # noqa: F401 from aiofiles.os import stat as stat_async # type: ignore # noqa: F401

View File

@@ -50,7 +50,7 @@ class StreamBuffer:
self._queue = asyncio.Queue(buffer_size) self._queue = asyncio.Queue(buffer_size)
async def read(self): async def read(self):
""" Stop reading when gets None """ """Stop reading when gets None"""
payload = await self._queue.get() payload = await self._queue.get()
self._queue.task_done() self._queue.task_done()
return payload return payload
@@ -265,9 +265,12 @@ class Request:
:type errors: str :type errors: str
:return: RequestParameters :return: RequestParameters
""" """
if not self.parsed_args[ if (
(keep_blank_values, strict_parsing, encoding, errors) keep_blank_values,
]: strict_parsing,
encoding,
errors,
) not in self.parsed_args:
if self.query_string: if self.query_string:
self.parsed_args[ self.parsed_args[
(keep_blank_values, strict_parsing, encoding, errors) (keep_blank_values, strict_parsing, encoding, errors)
@@ -321,9 +324,12 @@ class Request:
:type errors: str :type errors: str
:return: list :return: list
""" """
if not self.parsed_not_grouped_args[ if (
(keep_blank_values, strict_parsing, encoding, errors) keep_blank_values,
]: strict_parsing,
encoding,
errors,
) not in self.parsed_not_grouped_args:
if self.query_string: if self.query_string:
self.parsed_not_grouped_args[ self.parsed_not_grouped_args[
(keep_blank_values, strict_parsing, encoding, errors) (keep_blank_values, strict_parsing, encoding, errors)

View File

@@ -169,7 +169,11 @@ class HttpProtocol(asyncio.Protocol):
self.request_class = self.app.request_class or Request self.request_class = self.app.request_class or Request
self.is_request_stream = self.app.is_request_stream self.is_request_stream = self.app.is_request_stream
self._is_stream_handler = False self._is_stream_handler = False
self._not_paused = asyncio.Event(loop=deprecated_loop) self._not_paused = (
asyncio.Event()
if sys.version_info >= (3, 10)
else asyncio.Event(loop=deprecated_loop)
)
self._total_request_size = 0 self._total_request_size = 0
self._request_timeout_handler = None self._request_timeout_handler = None
self._response_timeout_handler = None self._response_timeout_handler = None

View File

@@ -1,7 +1,7 @@
from functools import partial, wraps from functools import partial, wraps
from mimetypes import guess_type from mimetypes import guess_type
from os import path from os import path, sep
from re import sub from pathlib import Path
from time import gmtime, strftime from time import gmtime, strftime
from urllib.parse import unquote from urllib.parse import unquote
@@ -26,28 +26,41 @@ async def _static_request_handler(
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(file_or_directory, sub("^[/]*", "", file_uri)) 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
# 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
)
try: try:
headers = {} headers = {}
# Check if the client has been sent this file before # Check if the client has been sent this file before

View File

@@ -81,14 +81,14 @@ env_dependency = (
'; sys_platform != "win32" ' 'and implementation_name == "cpython"' '; sys_platform != "win32" ' 'and implementation_name == "cpython"'
) )
ujson = "ujson>=1.35" + env_dependency ujson = "ujson>=1.35" + env_dependency
uvloop = "uvloop>=0.5.3,<0.15.0" + env_dependency uvloop = "uvloop>=0.5.3" + env_dependency
requirements = [ requirements = [
"httptools>=0.0.10", "httptools>=0.0.10",
uvloop, uvloop,
ujson, ujson,
"aiofiles>=0.6.0", "aiofiles>=0.6.0",
"websockets>=8.1,<9.0", "websockets>=8.1,<=9.1",
"multidict>=5.0,<6.0", "multidict>=5.0,<6.0",
"httpx==0.15.4", "httpx==0.15.4",
] ]

View File

@@ -1,6 +1,3 @@
import asyncio
import sys
from collections import deque, namedtuple from collections import deque, namedtuple
import pytest import pytest
@@ -82,14 +79,6 @@ def test_listeners_triggered(app):
with pytest.warns(UserWarning): with pytest.warns(UserWarning):
server.run() server.run()
all_tasks = (
asyncio.Task.all_tasks()
if sys.version_info < (3, 7)
else asyncio.all_tasks(asyncio.get_event_loop())
)
for task in all_tasks:
task.cancel()
assert before_server_start assert before_server_start
assert after_server_start assert after_server_start
assert before_server_stop assert before_server_stop
@@ -132,14 +121,6 @@ def test_listeners_triggered_async(app):
with pytest.warns(UserWarning): with pytest.warns(UserWarning):
server.run() server.run()
all_tasks = (
asyncio.Task.all_tasks()
if sys.version_info < (3, 7)
else asyncio.all_tasks(asyncio.get_event_loop())
)
for task in all_tasks:
task.cancel()
assert before_server_start assert before_server_start
assert after_server_start assert after_server_start
assert before_server_stop assert before_server_stop

View File

@@ -13,7 +13,7 @@ from sanic.exceptions import PyFileError
@contextmanager @contextmanager
def temp_path(): def temp_path():
""" a simple cross platform replacement for NamedTemporaryFile """ """a simple cross platform replacement for NamedTemporaryFile"""
with TemporaryDirectory() as td: with TemporaryDirectory() as td:
yield Path(td, "file") yield Path(td, "file")

View File

@@ -102,7 +102,7 @@ def test_logging_pass_customer_logconfig():
@pytest.mark.parametrize("debug", (True, False)) @pytest.mark.parametrize("debug", (True, False))
def test_log_connection_lost(app, debug, monkeypatch): def test_log_connection_lost(app, debug, monkeypatch):
""" Should not log Connection lost exception on non debug """ """Should not log Connection lost exception on non debug"""
stream = StringIO() stream = StringIO()
root = logging.getLogger("sanic.root") root = logging.getLogger("sanic.root")
root.addHandler(logging.StreamHandler(stream)) root.addHandler(logging.StreamHandler(stream))

View File

@@ -290,6 +290,17 @@ def test_query_string(app):
assert request.args.get("test3", default="My value") == "My value" assert request.args.get("test3", default="My value") == "My value"
def test_popped_stays_popped(app):
@app.route("/")
async def handler(request):
return text("OK")
request, response = app.test_client.get("/", params=[("test1", "1")])
assert request.args.pop("test1") == ["1"]
assert "test1" not in request.args
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_query_string_asgi(app): async def test_query_string_asgi(app):
@app.route("/") @app.route("/")

View File

@@ -1,10 +1,14 @@
import inspect import inspect
import os import os
import sys
from pathlib import Path
from time import gmtime, strftime from time import gmtime, strftime
import pytest import pytest
from sanic.app import Sanic
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def static_file_directory(): def static_file_directory():
@@ -15,6 +19,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)
@@ -374,3 +394,43 @@ def test_static_name(app, static_file_directory, static_name, file_name):
request, response = app.test_client.get(f"/static/{file_name}") request, response = app.test_client.get(f"/static/{file_name}")
assert response.status == 200 assert response.status == 200
@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

View File

@@ -20,7 +20,7 @@ deps =
beautifulsoup4 beautifulsoup4
gunicorn==20.0.4 gunicorn==20.0.4
uvicorn uvicorn
websockets>=8.1,<9.0 websockets>=8.1,<=9.1
commands = commands =
pytest {posargs:tests --cov sanic} pytest {posargs:tests --cov sanic}
- coverage combine --append - coverage combine --append