diff --git a/CHANGES.txt b/CHANGES.txt index c7f32ea4..a1b2dae3 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,11 @@ Unreleased Bugfix ~~~~~~ +- Waitress now normalizes a non-ASCII ``url_prefix`` to the PEP 3333 + encoding used for request paths, so the prefix matches the parsed + ``PATH_INFO``/``SCRIPT_NAME``. See + https://github.com/Pylons/waitress/pull/493. + - Renamed the HTTP header "Trailers" to "Trailer" to fix a typo and comply with the correct header name as specified in RFC 7230. diff --git a/src/waitress/adjustments.py b/src/waitress/adjustments.py index 8b26eb87..9389c4ed 100644 --- a/src/waitress/adjustments.py +++ b/src/waitress/adjustments.py @@ -75,6 +75,11 @@ def slash_fixed_str(s): # always have a leading slash, replace any number of leading slashes # with a single slash, and strip any trailing slashes s = "/" + s.lstrip("/").rstrip("/") + # Normalize encoding to match PEP 3333: WSGI PATH_INFO/SCRIPT_NAME + # are raw URL bytes decoded as ISO-8859-1. Encode the user-supplied + # Unicode string as UTF-8 and decode as ISO-8859-1 so that non-ASCII + # characters compare equal to the parser-produced path. + s = s.encode("utf-8").decode("iso-8859-1") return s diff --git a/tests/test_adjustments.py b/tests/test_adjustments.py index 86bf5ded..591b3ac4 100644 --- a/tests/test_adjustments.py +++ b/tests/test_adjustments.py @@ -384,6 +384,27 @@ def test_server_header_removable(self): inst = self._makeOne(ident="specific_header") self.assertEqual(inst.ident, "specific_header") + def test_url_prefix_unicode_normalization(self): + from waitress.parser import unquote_bytes_to_wsgi + + # Non-ASCII url_prefix should be normalized to PEP 3333 format + # (UTF-8-encoded bytes decoded as ISO-8859-1) so it compares + # equal to the parser-produced path for the same logical path. + inst = self._makeOne(url_prefix="/\xfc/") # ü as latin-1 byte + # \xfc (ü in latin-1) encoded as UTF-8 is \xc3\xbc. + # Decoded as latin-1 that gives two code points: \xc3 \xbc. + # Note: slash_fixed_str strips any trailing slash. + self.assertEqual(inst.url_prefix, "/ü") + + # ASCII-only url_prefix must still be unchanged. + inst2 = self._makeOne(url_prefix="/api") + self.assertEqual(inst2.url_prefix, "/api") + + # Now check it matches a parser-produced path for /ü + raw_uri = b"/%C3%BC" + path = unquote_bytes_to_wsgi(raw_uri) + self.assertEqual(inst.url_prefix, path) + class TestCLI(unittest.TestCase): def parse(self, argv):