diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..123824d --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,113 @@ +# The "build" workflow produces wheels (and the sdist) for all python +# versions/platforms. Where possible (i.e. the build is not a cross-compile), +# the test suite is also run for the wheel (this test covers fewer +# configurations than the "test" workflow and tox.ini). +name: Build + +on: + push: + branches: + # Run on release branches. This gives us a chance to detect rot in this + # configuration before pushing a tag (which we'd rather not have to undo). + - "branch[0-9]*" + tags: + # The main purpose of this workflow is to build wheels for release tags. + # It runs automatically on tags matching this pattern and pushes to pypi. + - "v*" + workflow_dispatch: + # Allow this workflow to be run manually (pushing to testpypi instead of pypi) + +permissions: {} + +env: + python-version: '3.9' + +jobs: + build_sdist: + name: Build sdist + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-python@v5 + name: Install Python + with: + python-version: ${{ env.python-version }} + + - name: Check metadata + run: "python setup.py check" + - name: Build sdist + run: "python setup.py sdist && ls -l dist" + + - uses: actions/upload-artifact@v4 + with: + name: artifacts-sdist + path: ./dist/tornado-*.tar.gz + + build_wheels: + name: Build wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-22.04, ubuntu-22.04-arm, windows-2022, macos-15] + + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-python@v5 + name: Install Python + with: + python-version: ${{ env.python-version }} + + - name: Build wheels + uses: pypa/cibuildwheel@v3.4.0 + + - name: Audit ABI3 compliance + # This may be moved into cibuildwheel itself in the future. See + # https://github.com/pypa/cibuildwheel/issues/1342 + run: "pip install abi3audit && abi3audit --verbose --summary ./wheelhouse/*.whl" + + - uses: actions/upload-artifact@v4 + with: + name: artifacts-${{ matrix.os }} + path: ./wheelhouse/*.whl + + upload_pypi_test: + name: Upload to PyPI (test) + needs: [build_wheels, build_sdist] + runs-on: ubuntu-22.04 + if: github.repository == 'tornadoweb/tornado' && github.event_name == 'workflow_dispatch' + permissions: + # This permission is required for pypi's "trusted publisher" feature + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: dist + merge-multiple: true + + - uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + skip-existing: true + + upload_pypi: + name: Upload to PyPI (prod) + needs: [build_wheels, build_sdist] + runs-on: ubuntu-22.04 + if: github.repository == 'tornadoweb/tornado' && github.event_name == 'push' && github.ref_type == 'tag' && startsWith(github.ref_name, 'v') + permissions: + # This permission is required for pypi's "trusted publisher" feature + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: dist + merge-multiple: true + + - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..6741867 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,152 @@ +# The "test" workflow is run on every PR and runs tests across all +# supported python versions and a range of configurations +# specified in tox.ini. Also see the "build" workflow which is only +# run for release branches and covers platforms other than linux-amd64 +# (Platform-specific issues are rare these days so we don't want to +# take that time on every build). + +name: Test + +on: pull_request + +permissions: {} + +jobs: + # Before starting the full build matrix, run one test configuration + # and the linter (the `black` linter is especially likely to catch + # first-time contributors). + test_quick: + name: Run quick tests + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-python@v5 + name: Install Python + with: + # Lint python version must be synced with tox.ini + python-version: '3.11' + - name: Install tox + run: python -m pip install tox -c requirements.txt + + - name: Run test suite + run: python -m tox -e py311,lint + + test_tox: + name: Run full tests + needs: test_quick + runs-on: ubuntu-22.04 + strategy: + matrix: + include: + - python: '3.9' + tox_env: py39-full + - python: '3.10' + tox_env: py310-full + - python: '3.10.8' + # Early versions of 3.10 and 3.11 had different deprecation + # warnings in asyncio. Test with them too to make sure everything + # works the same way. + tox_env: py310-full + - python: '3.11' + tox_env: py311-full + - python: '3.11.0' + tox_env: py311-full + - python: '3.12' + tox_env: py312-full + - python: '3.13' + tox_env: py313-full + - python: '3.14.0-beta.1 - 3.14' + tox_env: py314-full + - python: 'pypy-3.10' + # Pypy is a lot slower due to jit warmup costs, so don't run the + # "full" test config there. + tox_env: pypy3 + - python: '3.11' + # Docs python version must be synced with tox.ini + tox_env: docs + + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-python@v5 + name: Install Python + with: + python-version: ${{ matrix.python}} + - name: Install apt packages + run: sudo apt-get update && sudo apt-get install libcurl4-openssl-dev + - name: Install tox + run: python -m pip install tox -c requirements.txt + + - name: Run test suite + run: python -m tox -e ${{ matrix.tox_env }} + + test_win: + # Windows tests are fairly slow, so only run one configuration here. + # We test on windows but not mac because even though mac is a more + # fully-supported platform, it's similar enough to linux that we + # don't generally need to test it separately. Windows is different + # enough that we'll break it if we don't test it in CI. + name: Run windows tests + needs: test_quick + runs-on: windows-2022 + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/setup-python@v5 + name: Install Python + with: + python-version: '3.11' + - name: Run test suite + # TODO: figure out what's up with these log messages + run: py -m tornado.test --fail-if-logs=false + + zizmor: + name: Analyze action configs with zizmor + runs-on: ubuntu-22.04 + needs: test_quick + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@v5 + name: Install uv + - name: Run zizmor + run: uvx zizmor .github/workflows + + test_cibw: + # cibuildwheel is the tool that we use for release builds in build.yml. + # Run it in the every-PR workflow because it's slightly different from our + # regular build and this gives us easier ways to test freethreading changes. + # + # Note that test_cibw and test_tox both take about a minute to run, but test_tox runs + # more tests; test_cibw spends a lot of its time installing dependencies. Replacing + # test_tox with test_cibw would entail either increasing test runtime or reducing + # test coverage. + name: Test with cibuildwheel + runs-on: ubuntu-22.04 + needs: test_quick + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Run cibuildwheel + uses: pypa/cibuildwheel@v2.22 + env: + # For speed, we only build one python version and one arch. We throw away the wheels + # built here; the real build is defined in build.yml. + CIBW_ARCHS: native + CIBW_BUILD: cp313-manylinux* + + # Alternatively, uncomment the following lines (and replace the previous CIBW_BUILD) + # to test a freethreading build of python. + #CIBW_BUILD: cp313t-manylinux* + #CIBW_ENABLE: cpython-freethreading + # I don't understand what this does but auditwheel seems to fail in this configuration. + # Since we're throwing away the wheels here, just skip it. + # TODO: When we no longer need to disable this, we can enable freethreading in + # build.yml. + #CIBW_REPAIR_WHEEL_COMMAND: "" diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 0000000..a71e19f --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,14 @@ +rules: + unpinned-uses: + config: + policies: + # Allow trusted repositories to use ref-pinning instead of hash-pinning. + # + # Defaults, from + # https://github.com/woodruffw/zizmor/blob/7b4e76e94be2f4d7b455664ba5252b2b4458b91d/src/audit/unpinned_uses.rs#L172-L193 + actions/*: ref-pin + github/*: ref-pin + dependabot/*: ref-pin + # Additional trusted repositories + pypa/*: ref-pin + astral-sh/setup-uv: ref-pin \ No newline at end of file diff --git a/.readthedocs.yaml b/.readthedocs.yaml index d9b6cb2..aff82f8 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -3,7 +3,7 @@ version: 2 build: os: ubuntu-22.04 tools: - python: "3.8" + python: "3.11" sphinx: configuration: docs/conf.py diff --git a/debian/changelog b/debian/changelog index 202de20..ee8a80e 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,65 @@ +python-tornado (6.5.5-1) unstable; urgency=medium + + * Team upload. + * New upstream release. + - CVE-2026-31958: Introduces new limits on the size and complexity of + multipart bodies, including a default limit of 100 parts per request + to mitigate a possible DoS. It is also possible to disable parsing + multipart/form-data entirely if not required (closes: #1130507). + - The domain, path, and samesite arguments to RequestHandler.set_cookie + are now validated for illegal characters, which could be abused to + inject other attributes on the cookie. + - Carriage return characters are no longer accepted in multipart/form-data + headers. + * d/python3-tornado.lintian-overrides: Fix matches. + + -- Daniel Leidert Tue, 31 Mar 2026 01:01:46 +0200 + +python-tornado (6.5.4-1) unstable; urgency=medium + + * Team upload. + * debian/patches/Make-tests-compatible-with-curl-8.19.0.patch: new patch. + (Closes: #1129145) + + -- Carlos Henrique Lima Melara Fri, 06 Mar 2026 01:04:28 -0300 + +python-tornado (6.5.4-0.1) unstable; urgency=medium + + * Non-maintainer upload. + * New upstream release. + - CVE-2025-67724: Header injection and XSS via reason argument. + (Closes: #1122660) + - CVE-2025-67725: Quadratic DoS via Repeated Header Coalescing. + (Closes: #1122661) + - CVE-2025-67726: Quadratic DoS via Crafted Multipart Parameters. + (Closes: #1122663) + + -- Adrian Bunk Mon, 05 Jan 2026 13:12:01 +0200 + +python-tornado (6.5.2-3) unstable; urgency=medium + + * Team upload. + * Increase timeout to fix ftbfs issue on slow architectures. + Thanks to Aurelien Jarno . (Closes: #1117144) + + -- Bo YU Sat, 04 Oct 2025 18:42:36 +0800 + +python-tornado (6.5.2-2) unstable; urgency=medium + + * Uploading to unstable. + + -- Thomas Goirand Sun, 28 Sep 2025 11:43:57 +0200 + +python-tornado (6.5.2-1) experimental; urgency=medium + + * Team upload. + * New upstream release. + * Refreshed some patches. + * Removed pythonpath-autoreload-test.patch now useless. + * Removed CVE-2025-47287.patch applied upstream. + + -- Thomas Goirand Tue, 26 Aug 2025 11:13:28 +0200 + python-tornado (6.4.2-3) unstable; urgency=medium * Team upload. diff --git a/debian/patches/0006-Use-local-objects.inv-for-intersphinx-mapping.patch b/debian/patches/0006-Use-local-objects.inv-for-intersphinx-mapping.patch index 7f28a7a..109c57f 100644 --- a/debian/patches/0006-Use-local-objects.inv-for-intersphinx-mapping.patch +++ b/debian/patches/0006-Use-local-objects.inv-for-intersphinx-mapping.patch @@ -9,9 +9,11 @@ Forwarded: not-needed docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) +Index: python-tornado/docs/conf.py +=================================================================== --- python-tornado.orig/docs/conf.py +++ python-tornado/docs/conf.py -@@ -84,7 +84,7 @@ +@@ -85,7 +85,7 @@ latex_documents = [ ) ] diff --git a/debian/patches/0007-Higher-test_gc-timeout.patch b/debian/patches/0007-Higher-test_gc-timeout.patch index 2152d72..02b52fe 100644 --- a/debian/patches/0007-Higher-test_gc-timeout.patch +++ b/debian/patches/0007-Higher-test_gc-timeout.patch @@ -7,9 +7,11 @@ Forwarded: not-needed tornado/test/gen_test.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) +Index: python-tornado/tornado/test/gen_test.py +=================================================================== --- python-tornado.orig/tornado/test/gen_test.py +++ python-tornado/tornado/test/gen_test.py -@@ -967,7 +967,10 @@ +@@ -961,7 +961,10 @@ class RunnerGCTest(AsyncTestCase): self.io_loop.add_callback(callback) yield fut diff --git a/debian/patches/CVE-2025-47287.patch b/debian/patches/CVE-2025-47287.patch deleted file mode 100644 index 209722e..0000000 --- a/debian/patches/CVE-2025-47287.patch +++ /dev/null @@ -1,231 +0,0 @@ -From: Ben Darnell -Date: Thu, 8 May 2025 13:29:43 -0400 -Subject: httputil: Raise errors instead of logging in multipart/form-data - parsing - -We used to continue after logging an error, which allowed repeated -errors to spam the logs. The error raised here will still be logged, -but only once per request, consistent with other error handling in -Tornado. - -Origin: backport, https://github.com/tornadoweb/tornado/pull/3497 -Bug-Debian: https://bugs.debian.org/1105886 -Last-Update: 2025-05-18 ---- - tornado/httputil.py | 30 +++++++++++------------------- - tornado/test/httpserver_test.py | 4 ++-- - tornado/test/httputil_test.py | 13 ++++++++----- - tornado/web.py | 17 +++++++++++++---- - 4 files changed, 34 insertions(+), 30 deletions(-) - -diff --git a/tornado/httputil.py b/tornado/httputil.py -index ebdc805..090a977 100644 ---- a/tornado/httputil.py -+++ b/tornado/httputil.py -@@ -34,7 +34,6 @@ import unicodedata - from urllib.parse import urlencode, urlparse, urlunparse, parse_qsl - - from tornado.escape import native_str, parse_qs_bytes, utf8 --from tornado.log import gen_log - from tornado.util import ObjectDict, unicode_type - - -@@ -762,25 +761,22 @@ def parse_body_arguments( - """ - if content_type.startswith("application/x-www-form-urlencoded"): - if headers and "Content-Encoding" in headers: -- gen_log.warning( -- "Unsupported Content-Encoding: %s", headers["Content-Encoding"] -+ raise HTTPInputError( -+ "Unsupported Content-Encoding: %s" % headers["Content-Encoding"] - ) -- return - try: - # real charset decoding will happen in RequestHandler.decode_argument() - uri_arguments = parse_qs_bytes(body, keep_blank_values=True) - except Exception as e: -- gen_log.warning("Invalid x-www-form-urlencoded body: %s", e) -- uri_arguments = {} -+ raise HTTPInputError("Invalid x-www-form-urlencoded body: %s" % e) from e - for name, values in uri_arguments.items(): - if values: - arguments.setdefault(name, []).extend(values) - elif content_type.startswith("multipart/form-data"): - if headers and "Content-Encoding" in headers: -- gen_log.warning( -- "Unsupported Content-Encoding: %s", headers["Content-Encoding"] -+ raise HTTPInputError( -+ "Unsupported Content-Encoding: %s" % headers["Content-Encoding"] - ) -- return - try: - fields = content_type.split(";") - for field in fields: -@@ -789,9 +785,9 @@ def parse_body_arguments( - parse_multipart_form_data(utf8(v), body, arguments, files) - break - else: -- raise ValueError("multipart boundary not found") -+ raise HTTPInputError("multipart boundary not found") - except Exception as e: -- gen_log.warning("Invalid multipart/form-data: %s", e) -+ raise HTTPInputError("Invalid multipart/form-data: %s" % e) from e - - - def parse_multipart_form_data( -@@ -820,26 +816,22 @@ def parse_multipart_form_data( - boundary = boundary[1:-1] - final_boundary_index = data.rfind(b"--" + boundary + b"--") - if final_boundary_index == -1: -- gen_log.warning("Invalid multipart/form-data: no final boundary") -- return -+ raise HTTPInputError("Invalid multipart/form-data: no final boundary found") - parts = data[:final_boundary_index].split(b"--" + boundary + b"\r\n") - for part in parts: - if not part: - continue - eoh = part.find(b"\r\n\r\n") - if eoh == -1: -- gen_log.warning("multipart/form-data missing headers") -- continue -+ raise HTTPInputError("multipart/form-data missing headers") - headers = HTTPHeaders.parse(part[:eoh].decode("utf-8")) - disp_header = headers.get("Content-Disposition", "") - disposition, disp_params = _parse_header(disp_header) - if disposition != "form-data" or not part.endswith(b"\r\n"): -- gen_log.warning("Invalid multipart/form-data") -- continue -+ raise HTTPInputError("Invalid multipart/form-data") - value = part[eoh + 4 : -2] - if not disp_params.get("name"): -- gen_log.warning("multipart/form-data value missing name") -- continue -+ raise HTTPInputError("multipart/form-data missing name") - name = disp_params["name"] - if disp_params.get("filename"): - ctype = headers.get("Content-Type", "application/unknown") -diff --git a/tornado/test/httpserver_test.py b/tornado/test/httpserver_test.py -index 0b29a39..5d5fb13 100644 ---- a/tornado/test/httpserver_test.py -+++ b/tornado/test/httpserver_test.py -@@ -1131,9 +1131,9 @@ class GzipUnsupportedTest(GzipBaseTest, AsyncHTTPTestCase): - # Gzip support is opt-in; without it the server fails to parse - # the body (but parsing form bodies is currently just a log message, - # not a fatal error). -- with ExpectLog(gen_log, "Unsupported Content-Encoding"): -+ with ExpectLog(gen_log, ".*Unsupported Content-Encoding"): - response = self.post_gzip("foo=bar") -- self.assertEqual(json_decode(response.body), {}) -+ self.assertEqual(response.code, 400) - - - class StreamingChunkSizeTest(AsyncHTTPTestCase): -diff --git a/tornado/test/httputil_test.py b/tornado/test/httputil_test.py -index 975900a..9494d0c 100644 ---- a/tornado/test/httputil_test.py -+++ b/tornado/test/httputil_test.py -@@ -12,7 +12,6 @@ from tornado.httputil import ( - ) - from tornado.escape import utf8, native_str - from tornado.log import gen_log --from tornado.testing import ExpectLog - from tornado.test.util import ignore_deprecation - - import copy -@@ -195,7 +194,9 @@ Foo - b"\n", b"\r\n" - ) - args, files = form_data_args() -- with ExpectLog(gen_log, "multipart/form-data missing headers"): -+ with self.assertRaises( -+ HTTPInputError, msg="multipart/form-data missing headers" -+ ): - parse_multipart_form_data(b"1234", data, args, files) - self.assertEqual(files, {}) - -@@ -209,7 +210,7 @@ Foo - b"\n", b"\r\n" - ) - args, files = form_data_args() -- with ExpectLog(gen_log, "Invalid multipart/form-data"): -+ with self.assertRaises(HTTPInputError, msg="Invalid multipart/form-data"): - parse_multipart_form_data(b"1234", data, args, files) - self.assertEqual(files, {}) - -@@ -222,7 +223,7 @@ Foo--1234--""".replace( - b"\n", b"\r\n" - ) - args, files = form_data_args() -- with ExpectLog(gen_log, "Invalid multipart/form-data"): -+ with self.assertRaises(HTTPInputError, msg="Invalid multipart/form-data"): - parse_multipart_form_data(b"1234", data, args, files) - self.assertEqual(files, {}) - -@@ -236,7 +237,9 @@ Foo - b"\n", b"\r\n" - ) - args, files = form_data_args() -- with ExpectLog(gen_log, "multipart/form-data value missing name"): -+ with self.assertRaises( -+ HTTPInputError, msg="multipart/form-data value missing name" -+ ): - parse_multipart_form_data(b"1234", data, args, files) - self.assertEqual(files, {}) - -diff --git a/tornado/web.py b/tornado/web.py -index 0393964..8ec5601 100644 ---- a/tornado/web.py -+++ b/tornado/web.py -@@ -1751,6 +1751,14 @@ class RequestHandler(object): - try: - if self.request.method not in self.SUPPORTED_METHODS: - raise HTTPError(405) -+ -+ # If we're not in stream_request_body mode, this is the place where we parse the body. -+ if not _has_stream_request_body(self.__class__): -+ try: -+ self.request._parse_body() -+ except httputil.HTTPInputError as e: -+ raise HTTPError(400, "Invalid body: %s" % e) from e -+ - self.path_args = [self.decode_argument(arg) for arg in args] - self.path_kwargs = dict( - (k, self.decode_argument(v, name=k)) for (k, v) in kwargs.items() -@@ -1941,7 +1949,7 @@ def _has_stream_request_body(cls: Type[RequestHandler]) -> bool: - - - def removeslash( -- method: Callable[..., Optional[Awaitable[None]]] -+ method: Callable[..., Optional[Awaitable[None]]], - ) -> Callable[..., Optional[Awaitable[None]]]: - """Use this decorator to remove trailing slashes from the request path. - -@@ -1970,7 +1978,7 @@ def removeslash( - - - def addslash( -- method: Callable[..., Optional[Awaitable[None]]] -+ method: Callable[..., Optional[Awaitable[None]]], - ) -> Callable[..., Optional[Awaitable[None]]]: - """Use this decorator to add a missing trailing slash to the request path. - -@@ -2394,8 +2402,9 @@ class _HandlerDelegate(httputil.HTTPMessageDelegate): - if self.stream_request_body: - future_set_result_unless_cancelled(self.request._body_future, None) - else: -+ # Note that the body gets parsed in RequestHandler._execute so it can be in -+ # the right exception handler scope. - self.request.body = b"".join(self.chunks) -- self.request._parse_body() - self.execute() - - def on_connection_close(self) -> None: -@@ -3267,7 +3276,7 @@ class GZipContentEncoding(OutputTransform): - - - def authenticated( -- method: Callable[..., Optional[Awaitable[None]]] -+ method: Callable[..., Optional[Awaitable[None]]], - ) -> Callable[..., Optional[Awaitable[None]]]: - """Decorate methods with this to require that the user be logged in. - diff --git a/debian/patches/Make-tests-compatible-with-curl-8.19.0.patch b/debian/patches/Make-tests-compatible-with-curl-8.19.0.patch new file mode 100644 index 0000000..32be9df --- /dev/null +++ b/debian/patches/Make-tests-compatible-with-curl-8.19.0.patch @@ -0,0 +1,41 @@ +From: Carlos Henrique Lima Melara +Date: Fri, 6 Mar 2026 00:56:17 -0300 +Subject: Make tests compatible with curl 8.19.0 + +In 8.19.0-rc2, the error logic has been changed so any later errors are +preserved. This changes what is returned by curl and therefore what tornado +sees. For HTTPError variant of the test, which uses CurlAsyncHTTPClient, we get +the error from pycurl and now it contains "Failed binding local connection +end". This logic handles both the old version of libcurl and also the newer +one. + +Co-Authored-By: Samuel Henrique + +Forwarded: yes, https://github.com/tornadoweb/tornado/pull/3582 +Last-Update: 2026-03-07 +--- + tornado/test/httpclient_test.py | 11 ++++++++++- + 1 file changed, 10 insertions(+), 1 deletion(-) + +diff --git a/tornado/test/httpclient_test.py b/tornado/test/httpclient_test.py +index 77c0d6e..060e16f 100644 +--- a/tornado/test/httpclient_test.py ++++ b/tornado/test/httpclient_test.py +@@ -622,7 +622,16 @@ X-XSS-Protection: 1; + with self.assertRaises((ValueError, HTTPError)) as context: # type: ignore + request = HTTPRequest(url, network_interface="not-interface-or-ip") + yield self.http_client.fetch(request) +- self.assertIn("not-interface-or-ip", str(context.exception)) ++ assert ( ++ any( ++ error_message in str(context.exception) ++ for error_message in [ ++ "Failed binding local connection end", ++ "not-interface-or-ip", ++ ] ++ ) ++ == True ++ ) + + def test_all_methods(self): + for method in ["GET", "DELETE", "OPTIONS"]: diff --git a/debian/patches/disable-domain-tests.patch b/debian/patches/disable-domain-tests.patch index 987f89c..435d76b 100644 --- a/debian/patches/disable-domain-tests.patch +++ b/debian/patches/disable-domain-tests.patch @@ -7,15 +7,15 @@ Forwarded: not-needed tornado/test/netutil_test.py | 1 + 1 file changed, 1 insertion(+) -diff --git a/tornado/test/netutil_test.py b/tornado/test/netutil_test.py -index b35b794..383d482 100644 ---- a/tornado/test/netutil_test.py -+++ b/tornado/test/netutil_test.py -@@ -59,6 +59,7 @@ class _ResolverTestMixin(object): - class _ResolverErrorTestMixin(object): +Index: python-tornado/tornado/test/netutil_test.py +=================================================================== +--- python-tornado.orig/tornado/test/netutil_test.py ++++ python-tornado/tornado/test/netutil_test.py +@@ -49,6 +49,7 @@ class _ResolverTestMixin(AsyncTestCase): + class _ResolverErrorTestMixin(AsyncTestCase): resolver = None # type: typing.Any + @unittest.skip("Prevent internet access during build") @gen_test - def test_bad_host(self: typing.Any): + def test_bad_host(self): with self.assertRaises(IOError): diff --git a/debian/patches/disable-should-be-failing-test.patch b/debian/patches/disable-should-be-failing-test.patch index 70d86b2..4a6fa17 100644 --- a/debian/patches/disable-should-be-failing-test.patch +++ b/debian/patches/disable-should-be-failing-test.patch @@ -1,6 +1,8 @@ ---- a/tornado/test/tcpclient_test.py -+++ b/tornado/test/tcpclient_test.py -@@ -155,6 +155,7 @@ +Index: python-tornado/tornado/test/tcpclient_test.py +=================================================================== +--- python-tornado.orig/tornado/test/tcpclient_test.py ++++ python-tornado/tornado/test/tcpclient_test.py +@@ -150,6 +150,7 @@ class TCPClientTest(AsyncTestCase): self.do_test_connect(socket.AF_INET, "127.0.0.1", source_ip="127.0.0.1") @skipIfNonUnix diff --git a/debian/patches/ignoreuserwarning.patch b/debian/patches/ignoreuserwarning.patch index eafb325..3ec004b 100644 --- a/debian/patches/ignoreuserwarning.patch +++ b/debian/patches/ignoreuserwarning.patch @@ -10,9 +10,11 @@ Forwarded: not-needed tornado/test/runtests.py | 1 + 1 file changed, 1 insertion(+) +Index: python-tornado/tornado/test/runtests.py +=================================================================== --- python-tornado.orig/tornado/test/runtests.py +++ python-tornado/tornado/test/runtests.py -@@ -121,6 +121,7 @@ +@@ -133,6 +133,7 @@ def main(): # setuptools sometimes gives ImportWarnings about things that are on # sys.path even if they're not being used. warnings.filterwarnings("ignore", category=ImportWarning) diff --git a/debian/patches/increase-timeout-httpclinet-test.patch b/debian/patches/increase-timeout-httpclinet-test.patch new file mode 100644 index 0000000..03d652c --- /dev/null +++ b/debian/patches/increase-timeout-httpclinet-test.patch @@ -0,0 +1,18 @@ +Description: increase timeout on simple_httpclient_test +Author: Aurelien Jarno +Bug: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1117144 +Forwarded: not-needed +Last-Update: 2025-10-04 +--- +This patch header follows DEP-3: http://dep.debian.net/deps/dep3/ +--- a/tornado/test/simple_httpclient_test.py ++++ b/tornado/test/simple_httpclient_test.py +@@ -316,7 +316,7 @@ + yield gen.sleep(0.2) + + def test_request_timeout(self): +- timeout = 0.1 ++ timeout = 0.2 + if os.name == "nt": + timeout = 0.5 + diff --git a/debian/patches/increase-timeout-rv64.patch b/debian/patches/increase-timeout-rv64.patch index 4381ced..8bdbf0b 100644 --- a/debian/patches/increase-timeout-rv64.patch +++ b/debian/patches/increase-timeout-rv64.patch @@ -5,9 +5,11 @@ Forwarded: not-needed Last-Update: 2025-05-19 --- This patch header follows DEP-3: http://dep.debian.net/deps/dep3/ ---- a/tornado/test/autoreload_test.py -+++ b/tornado/test/autoreload_test.py -@@ -7,6 +7,7 @@ +Index: python-tornado/tornado/test/autoreload_test.py +=================================================================== +--- python-tornado.orig/tornado/test/autoreload_test.py ++++ python-tornado/tornado/test/autoreload_test.py +@@ -7,6 +7,7 @@ from tempfile import mkdtemp import textwrap import time import unittest @@ -15,7 +17,7 @@ This patch header follows DEP-3: http://dep.debian.net/deps/dep3/ class AutoreloadTest(unittest.TestCase): -@@ -91,7 +92,10 @@ +@@ -95,7 +96,10 @@ class AutoreloadTest(unittest.TestCase): for i in range(40): if p.poll() is not None: break diff --git a/debian/patches/pythonpath-autoreload-test.patch b/debian/patches/pythonpath-autoreload-test.patch deleted file mode 100644 index 00000b9..0000000 --- a/debian/patches/pythonpath-autoreload-test.patch +++ /dev/null @@ -1,23 +0,0 @@ -From: Stefano Rivera -Date: Sun, 21 Jan 2024 15:55:16 -0400 -Subject: autoreload_test: Handle a relative PYTHONPATH - -This came up in the Debian package build of tornado, where we run the -tests from a staged build of the module. - -Forwarded: https://github.com/tornadoweb/tornado/pull/3358 ---- - tornado/test/autoreload_test.py | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - ---- python-tornado.orig/tornado/test/autoreload_test.py -+++ python-tornado/tornado/test/autoreload_test.py -@@ -75,7 +75,7 @@ - # application - pythonpath = os.getcwd() - if "PYTHONPATH" in os.environ: -- pythonpath += os.pathsep + os.environ["PYTHONPATH"] -+ pythonpath += os.pathsep + os.path.join(os.getcwd(), os.environ["PYTHONPATH"]) - - p = Popen( - args, diff --git a/debian/patches/series b/debian/patches/series index d9af7fc..2ef7940 100644 --- a/debian/patches/series +++ b/debian/patches/series @@ -2,7 +2,7 @@ disable-domain-tests.patch ignoreuserwarning.patch 0006-Use-local-objects.inv-for-intersphinx-mapping.patch 0007-Higher-test_gc-timeout.patch -pythonpath-autoreload-test.patch disable-should-be-failing-test.patch -CVE-2025-47287.patch increase-timeout-rv64.patch +increase-timeout-httpclinet-test.patch +Make-tests-compatible-with-curl-8.19.0.patch diff --git a/debian/python3-tornado.lintian-overrides b/debian/python3-tornado.lintian-overrides index 0f11445..c5042ca 100644 --- a/debian/python3-tornado.lintian-overrides +++ b/debian/python3-tornado.lintian-overrides @@ -1,9 +1,9 @@ # webserver belongs to web (#665854) python3-tornado binary: wrong-section-according-to-package-name # test sample, so duplication shouldn't be a problem -python3-tornado: compressed-duplicate usr/lib/python3/dist-packages/tornado/test/static/sample.xml.bz2 -python3-tornado: compressed-duplicate usr/lib/python3/dist-packages/tornado/test/static/sample.xml.gz +python3-tornado: compressed-duplicate [usr/lib/python3/dist-packages/tornado/test/static/sample.xml.bz2] +python3-tornado: compressed-duplicate [usr/lib/python3/dist-packages/tornado/test/static/sample.xml.gz] # it's not documentation just because it has a .html/.txt suffix! -python3-tornado: package-contains-documentation-outside-usr-share-doc usr/lib/python3/dist-packages/tornado/test/static/dir/index.html -python3-tornado: package-contains-documentation-outside-usr-share-doc usr/lib/python3/dist-packages/tornado/test/static/robots.txt -python3-tornado: package-contains-documentation-outside-usr-share-doc usr/lib/python3/dist-packages/tornado/test/static_foo.txt +python3-tornado: package-contains-documentation-outside-usr-share-doc [usr/lib/python3/dist-packages/tornado/test/static/dir/index.html] +python3-tornado: package-contains-documentation-outside-usr-share-doc [usr/lib/python3/dist-packages/tornado/test/static/robots.txt] +python3-tornado: package-contains-documentation-outside-usr-share-doc [usr/lib/python3/dist-packages/tornado/test/static_foo.txt] diff --git a/demos/README.rst b/demos/README.rst index 0429761..cf7cdb3 100644 --- a/demos/README.rst +++ b/demos/README.rst @@ -5,16 +5,6 @@ This directory contains several example apps that illustrate the usage of various Tornado features. If you're not sure where to start, try the ``chat``, ``blog``, or ``websocket`` demos. -.. note:: - - These applications require features due to be introduced in Tornado 6.3 - which is not yet released. Unless you are testing the new release, - use the GitHub branch selector to access the ``stable`` branch - (or the ``branchX.y`` branch corresponding to the version of Tornado you - are using) to get a suitable version of the demos. - - TODO: remove this when 6.3 ships. - Web Applications ~~~~~~~~~~~~~~~~ @@ -24,7 +14,6 @@ Web Applications - ``websocket``: Similar to ``chat`` but with WebSockets instead of long polling. - ``helloworld``: The simplest possible Tornado web page. -- ``s3server``: Implements a basic subset of the Amazon S3 API. Feature demos ~~~~~~~~~~~~~ diff --git a/demos/blog/blog.py b/demos/blog/blog.py index e6e23f8..cfa84f9 100755 --- a/demos/blog/blog.py +++ b/demos/blog/blog.py @@ -132,6 +132,14 @@ async def prepare(self): async def any_author_exists(self): return bool(await self.query("SELECT * FROM authors LIMIT 1")) + def redirect_to_next(self): + next = self.get_argument("next", "/") + if next.startswith("//") or not next.startswith("/"): + # Absolute URLs are not allowed because this would be an open redirect + # vulnerability (https://cwe.mitre.org/data/definitions/601.html). + raise tornado.web.HTTPError(400) + self.redirect(next) + class HomeHandler(BaseHandler): async def get(self): @@ -243,7 +251,7 @@ async def post(self): tornado.escape.to_unicode(hashed_password), ) self.set_signed_cookie("blogdemo_user", str(author.id)) - self.redirect(self.get_argument("next", "/")) + self.redirect_to_next() class AuthLoginHandler(BaseHandler): @@ -270,7 +278,7 @@ async def post(self): ) if password_equal: self.set_signed_cookie("blogdemo_user", str(author.id)) - self.redirect(self.get_argument("next", "/")) + self.redirect_to_next() else: self.render("login.html", error="incorrect password") @@ -278,7 +286,7 @@ async def post(self): class AuthLogoutHandler(BaseHandler): def get(self): self.clear_cookie("blogdemo_user") - self.redirect(self.get_argument("next", "/")) + self.redirect_to_next() class EntryModule(tornado.web.UIModule): diff --git a/demos/blog/templates/base.html b/demos/blog/templates/base.html index e21f29a..0c94e52 100644 --- a/demos/blog/templates/base.html +++ b/demos/blog/templates/base.html @@ -1,27 +1,31 @@ - - - {{ escape(handler.settings["blog_title"]) }} - - - {% block head %}{% end %} - - -
-