Skip to content

Commit

Permalink
Merge branch 'main' into vsock
Browse files Browse the repository at this point in the history
  • Loading branch information
digitalresistor committed Feb 4, 2024
2 parents e07b315 + 5934be0 commit c6df686
Show file tree
Hide file tree
Showing 10 changed files with 105 additions and 79 deletions.
28 changes: 22 additions & 6 deletions .github/workflows/ci-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ jobs:
strategy:
matrix:
py:
- "3.7"
- "3.8"
- "3.9"
- "3.10"
- "3.11"
- "3.12"
- "pypy-3.8"
- "pypy-3.9"
- "pypy-3.10"
# Pre-release
- "3.11.0-alpha - 3.11.0"
os:
- "ubuntu-latest"
- "windows-latest"
Expand All @@ -32,19 +34,33 @@ jobs:
include:
- py: "pypy-3.8"
toxenv: "pypy38"
- py: "pypy-3.9"
toxenv: "pypy39"
- py: "pypy-3.10"
toxenv: "pypy310"
exclude:
# Linux and macOS don't have x86 python
- os: "ubuntu-latest"
architecture: x86
- os: "macos-latest"
architecture: x86
# Don't run all PyPy versions except latest on
# Windows/macOS. They are expensive to run.
- os: "windows-latest"
py: "pypy-3.8"
- os: "macos-latest"
py: "pypy-3.8"
- os: "windows-latest"
py: "pypy-3.9"
- os: "macos-latest"
py: "pypy-3.9"

name: "Python: ${{ matrix.py }}-${{ matrix.architecture }} on ${{ matrix.os }}"
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Setup python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.py }}
architecture: ${{ matrix.architecture }}
Expand All @@ -64,7 +80,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup python 3.10
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: "3.10"
architecture: x64
Expand All @@ -77,7 +93,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: "3.10"
architecture: x64
Expand All @@ -89,7 +105,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Setup python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: "3.10"
architecture: x64
Expand Down
14 changes: 5 additions & 9 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,18 @@ Waitress
:target: https://pypi.org/project/waitress/
:alt: latest version of waitress on PyPI

.. image:: https://github.com/Pylons/waitress/workflows/Build%20and%20test/badge.svg
:target: https://github.com/Pylons/waitress/actions?query=workflow%3A%22Build+and+test%22
.. image:: https://github.com/Pylons/waitress/actions/workflows/ci-tests.yml/badge.svg
:target: https://github.com/Pylons/waitress/actions/workflows/ci-tests.yml

.. image:: https://readthedocs.org/projects/waitress/badge/?version=main
:target: https://docs.pylonsproject.org/projects/waitress/en/main
:alt: main Documentation Status

.. image:: https://img.shields.io/badge/irc-freenode-blue.svg
:target: https://webchat.freenode.net/?channels=pyramid
:alt: IRC Freenode

Waitress is a production-quality pure-Python WSGI server with very acceptable
performance. It has no dependencies except ones which live in the Python
standard library. It runs on CPython on Unix and Windows under Python 3.7+. It
is also known to run on PyPy 3 (version 3.7 compatible python) on UNIX. It
supports HTTP/1.0 and HTTP/1.1.
standard library. It runs on CPython on Unix and Windows under Python 3.8+. It
is also known to run on PyPy 3 (version 3.8 compatible python and above) on
UNIX. It supports HTTP/1.0 and HTTP/1.1.

For more information, see the "docs" directory of the Waitress package or visit
https://docs.pylonsproject.org/projects/waitress/en/latest/
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Waitress
Waitress is meant to be a production-quality pure-Python WSGI server with very
acceptable performance. It has no dependencies except ones which live in the
Python standard library. It runs on CPython on Unix and Windows under Python
3.7+. It is also known to run on PyPy 3 (python version 3.7+) on UNIX. It
3.8+. It is also known to run on PyPy 3 (python version 3.8+) on UNIX. It
supports HTTP/1.0 and HTTP/1.1.


Expand Down
6 changes: 3 additions & 3 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ classifiers =
License :: OSI Approved :: Zope Public License
Programming Language :: Python
Programming Language :: Python :: 3
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Programming Language :: Python :: 3.12
Programming Language :: Python :: Implementation :: CPython
Programming Language :: Python :: Implementation :: PyPy
Operating System :: OS Independent
Expand All @@ -37,7 +38,7 @@ maintainer_email = pylons-discuss@googlegroups.com
package_dir=
=src
packages=find:
python_requires = >=3.7.0
python_requires = >=3.8.0

[options.entry_points]
paste.server_runner =
Expand All @@ -58,7 +59,6 @@ docs =
Sphinx>=1.8.1
docutils
pylons-sphinx-themes>=1.0.9
importlib-metadata; python_version < "3.8"

[tool:pytest]
python_files = test_*.py
Expand Down
48 changes: 24 additions & 24 deletions src/waitress/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,9 @@ def parse_header(self, header_plus):

# command, uri, version will be bytes
command, uri, version = crack_first_line(first_line)
if command == uri == version == b"":
raise ParsingError("Start line is invalid")

# self.request_uri is like nginx's request_uri:
# "full original request URI (with arguments)"
self.request_uri = uri.decode("latin-1")
Expand Down Expand Up @@ -407,36 +410,33 @@ def get_header_lines(header):


first_line_re = re.compile(
b"([^ ]+) "
b"((?:[^ :?#]+://[^ ?#/]*(?:[0-9]{1,5})?)?[^ ]+)"
b"(( HTTP/([0-9.]+))$|$)"
rb"(?P<method>[!#$%&'*+\-.^_`|~0-9A-Za-z]+) "
rb"(?P<uri>(?:[^ :?#]+://[^ ?#/]*(?:[0-9]{1,5})?)?[^ ]+)"
rb"(?: HTTP/(?P<version>[0-9]\.[0-9]))?"
)


def crack_first_line(line):
m = first_line_re.match(line)
m = first_line_re.fullmatch(line)

if m is not None and m.end() == len(line):
if m.group(3):
version = m.group(5)
else:
version = b""
method = m.group(1)
if m is None:
return b"", b"", b""

# the request methods that are currently defined are all uppercase:
# https://www.iana.org/assignments/http-methods/http-methods.xhtml and
# the request method is case sensitive according to
# https://tools.ietf.org/html/rfc7231#section-4.1
version = m["version"] or b""
method = m["method"]
uri = m["uri"]

# By disallowing anything but uppercase methods we save poor
# unsuspecting souls from sending lowercase HTTP methods to waitress
# and having the request complete, while servers like nginx drop the
# request onto the floor.
# the request methods that are currently defined are all uppercase:
# https://www.iana.org/assignments/http-methods/http-methods.xhtml and
# the request method is case sensitive according to
# https://tools.ietf.org/html/rfc7231#section-4.1

if method != method.upper():
raise ParsingError('Malformed HTTP method "%s"' % str(method, "latin-1"))
uri = m.group(2)
# By disallowing anything but uppercase methods we save poor
# unsuspecting souls from sending lowercase HTTP methods to waitress
# and having the request complete, while servers like nginx drop the
# request onto the floor.

return method, uri, version
else:
return b"", b"", b""
if method != method.upper():
raise ParsingError('Malformed HTTP method "%s"' % str(method, "latin-1"))

return method, uri, version
66 changes: 34 additions & 32 deletions src/waitress/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,18 @@ def has_body(self):
or self.status.startswith("304")
)

def set_close_on_finish(self) -> None:
# if headers have not been written yet, tell the remote
# client we are closing the connection
if not self.wrote_header:
connection_close_header = None
for headername, headerval in self.response_headers:
if headername.capitalize() == "Connection":
connection_close_header = headerval.lower()
if connection_close_header is None:
self.response_headers.append(("Connection", "close"))
self.close_on_finish = True

def build_response_header(self):
version = self.version
# Figure out whether the connection should be closed.
Expand All @@ -187,7 +199,6 @@ def build_response_header(self):
content_length_header = None
date_header = None
server_header = None
connection_close_header = None

for headername, headerval in self.response_headers:
headername = "-".join([x.capitalize() for x in headername.split("-")])
Expand All @@ -204,47 +215,43 @@ def build_response_header(self):
if headername == "Server":
server_header = headerval

if headername == "Connection":
connection_close_header = headerval.lower()
# replace with properly capitalized version
response_headers.append((headername, headerval))

# Overwrite the response headers we have with normalized ones
self.response_headers = response_headers

if (
content_length_header is None
and self.content_length is not None
and self.has_body
):
content_length_header = str(self.content_length)
response_headers.append(("Content-Length", content_length_header))

def close_on_finish():
if connection_close_header is None:
response_headers.append(("Connection", "close"))
self.close_on_finish = True
self.response_headers.append(("Content-Length", content_length_header))

if version == "1.0":
if connection == "keep-alive":
if not content_length_header:
close_on_finish()
self.set_close_on_finish()
else:
response_headers.append(("Connection", "Keep-Alive"))
self.response_headers.append(("Connection", "Keep-Alive"))
else:
close_on_finish()
self.set_close_on_finish()

elif version == "1.1":
if connection == "close":
close_on_finish()
self.set_close_on_finish()

if not content_length_header:
# RFC 7230: MUST NOT send Transfer-Encoding or Content-Length
# for any response with a status code of 1xx, 204 or 304.

if self.has_body:
response_headers.append(("Transfer-Encoding", "chunked"))
self.response_headers.append(("Transfer-Encoding", "chunked"))
self.chunked_response = True

if not self.close_on_finish:
close_on_finish()
self.set_close_on_finish()

# under HTTP 1.1 keep-alive is default, no need to set the header
else:
Expand All @@ -256,14 +263,12 @@ def close_on_finish():

if not server_header:
if ident:
response_headers.append(("Server", ident))
self.response_headers.append(("Server", ident))
else:
response_headers.append(("Via", ident or "waitress"))
self.response_headers.append(("Via", ident or "waitress"))

if not date_header:
response_headers.append(("Date", build_http_date(self.start_time)))

self.response_headers = response_headers
self.response_headers.append(("Date", build_http_date(self.start_time)))

first_line = f"HTTP/{self.version} {self.status}"
# NB: sorting headers needs to preserve same-named-header order
Expand Down Expand Up @@ -349,11 +354,7 @@ def execute(self):
status, headers, body = e.to_response(ident)
self.status = status
self.response_headers.extend(headers)
# We need to explicitly tell the remote client we are closing the
# connection, because self.close_on_finish is set, and we are going to
# slam the door in the clients face.
self.response_headers.append(("Connection", "close"))
self.close_on_finish = True
self.set_close_on_finish()
self.content_length = len(body)
self.write(body)

Expand Down Expand Up @@ -387,7 +388,7 @@ def start_response(status, headers, exc_info=None):

self.complete = True

if not status.__class__ is str:
if status.__class__ is not str:
raise AssertionError("status %s is not a string" % status)
if "\n" in status or "\r" in status:
raise ValueError(
Expand All @@ -398,11 +399,11 @@ def start_response(status, headers, exc_info=None):

# Prepare the headers for output
for k, v in headers:
if not k.__class__ is str:
if k.__class__ is not str:
raise AssertionError(
f"Header name {k!r} is not a string in {(k, v)!r}"
)
if not v.__class__ is str:
if v.__class__ is not str:
raise AssertionError(
f"Header value {v!r} is not a string in {(k, v)!r}"
)
Expand Down Expand Up @@ -473,16 +474,17 @@ def start_response(status, headers, exc_info=None):

cl = self.content_length
if cl is not None:
if self.content_bytes_written != cl:
if self.content_bytes_written != cl and self.request.command != "HEAD":
# close the connection so the client isn't sitting around
# waiting for more data when there are too few bytes
# to service content-length
self.close_on_finish = True
self.set_close_on_finish()
if self.request.command != "HEAD":
self.logger.warning(
"application returned too few bytes (%s) "
"for specified Content-Length (%s) via app_iter"
% (self.content_bytes_written, cl),
"for specified Content-Length (%s) via app_iter",
self.content_bytes_written,
cl,
)
finally:
if can_close_app_iter and hasattr(app_iter, "close"):
Expand Down
8 changes: 8 additions & 0 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,14 @@ def test_crack_first_line_missing_version(self):
result = self._callFUT(b"GET /")
self.assertEqual(result, (b"GET", b"/", b""))

def test_crack_first_line_bad_method(self):
result = self._callFUT(b"GE\x00 /foobar HTTP/8.4")
self.assertEqual(result, (b"", b"", b""))

def test_crack_first_line_bad_version(self):
result = self._callFUT(b"GET /foobar HTTP/.1.")
self.assertEqual(result, (b"", b"", b""))


class TestHTTPRequestParserIntegration(unittest.TestCase):
def setUp(self):
Expand Down
Loading

0 comments on commit c6df686

Please sign in to comment.