Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Doc/library/wsgiref.rst
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,14 @@
:pep:`3333`.


.. method:: WSGIRequestHandler.get_stdin()

Return the object that should be used as the ``wsgi.input`` stream. If the

Check warning on line 376 in Doc/library/wsgiref.rst

View workflow job for this annotation

GitHub Actions / Docs / Docs

py:attr reference target not found: rfile [ref.attr]
request provides a ``Content-Length`` header, the default implementation returns
a wrapper around :attr:`rfile` that limits reads to that many bytes. Otherwise,
:attr:`rfile` is returned unchanged.


.. method:: WSGIRequestHandler.get_stderr()

Return the object that should be used as the ``wsgi.errors`` stream. The default
Expand Down
88 changes: 81 additions & 7 deletions Lib/test/test_wsgiref.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,18 +179,92 @@ def bad_app(environ, start_response):
)
self.assertEqual(err.splitlines()[-2], exc_message)

def test_wsgi_input(self):
def bad_app(e,s):
@force_not_colorized
def test_wsgi_input_validation(self):
def app(e, s):
e["wsgi.input"].read()
s("200 OK", [("Content-Type", "text/plain; charset=utf-8")])
return [b"data"]
out, err = run_amock(validator(bad_app))
self.assertEndsWith(out,
b"A server error occurred. Please contact the administrator."
out, err = run_amock(validator(app))
self.assertEqual(out.splitlines()[-1], b"data")
self.assertEndsWith(err, '"GET / HTTP/1.0" 200 4\n')

@force_not_colorized
def test_wsgi_input_read(self):
def app(e, s):
s("200 OK", [("Content-Type", "text/plain; charset=utf-8")])
return [e["wsgi.input"].read(3), b"-", e["wsgi.input"].read()]
request = (
b"POST / HTTP/1.0\n"
b"Content-Length: 6\n\n"
b"foobarEXTRA"
)
self.assertEqual(
err.splitlines()[-2], "AssertionError"
out, err = run_amock(app, request)
self.assertEqual(out.splitlines()[-1], b"foo-bar")
self.assertEndsWith(err, '"POST / HTTP/1.0" 200 7\n')

@force_not_colorized
def test_wsgi_input_readline(self):
def app(e, s):
s("200 OK", [("Content-Type", "text/plain; charset=utf-8")])
return [
e["wsgi.input"].readline(3),
b"-",
e["wsgi.input"].readline(),
e["wsgi.input"].readline(),
]
request = (
b"POST / HTTP/1.0\n"
b"Content-Length: 10\n\n"
b"foobar\n"
b"bazEXTRA"
)
out, err = run_amock(app, request)
self.assertEqual(out.splitlines()[-2], b"foo-bar")
self.assertEqual(out.splitlines()[-1], b"baz")
self.assertEndsWith(err, '"POST / HTTP/1.0" 200 11\n')

@force_not_colorized
def test_wsgi_input_readlines(self):
def app(e, s):
s("200 OK", [("Content-Type", "text/plain; charset=utf-8")])
return (
e["wsgi.input"].readlines(3)
+ [b"-"]
+ e["wsgi.input"].readlines()
)
request = (
b"POST / HTTP/1.0\n"
b"Content-Length: 17\n\n"
b"foobar\n"
b"baz\n"
b"hello\n"
b"EXTRA"
)
out, err = run_amock(app, request)
self.assertEqual(out.splitlines()[-3], b"foobar")
self.assertEqual(out.splitlines()[-2], b"-baz")
self.assertEqual(out.splitlines()[-1], b"hello")
self.assertEndsWith(err, '"POST / HTTP/1.0" 200 18\n')

@force_not_colorized
def test_wsgi_input_iter(self):
def app(e, s):
s("200 OK", [("Content-Type", "text/plain; charset=utf-8")])
return e["wsgi.input"]
request = (
b"POST / HTTP/1.0\n"
b"Content-Length: 17\n\n"
b"foobar\n"
b"baz\n"
b"hello\n"
b"EXTRA"
)
out, err = run_amock(app, request)
self.assertEqual(out.splitlines()[-3], b"foobar")
self.assertEqual(out.splitlines()[-2], b"baz")
self.assertEqual(out.splitlines()[-1], b"hello")
self.assertEndsWith(err, '"POST / HTTP/1.0" 200 17\n')

@force_not_colorized
def test_bytes_validation(self):
Expand Down
47 changes: 45 additions & 2 deletions Lib/wsgiref/simple_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,42 @@ def set_app(self,application):
self.application = application


class InputWrapper:

def __init__(self, stream, remaining):
self.stream = stream
self.remaining = remaining

def read(self, size=-1, /):
readable = min(size, self.remaining) if size >= 0 else self.remaining
if readable == 0:
return b''
data = self.stream.read(readable)
self.remaining -= readable
return data

def readline(self, size=-1, /):
readable = min(size, self.remaining) if size >= 0 else self.remaining
if readable == 0:
return b''
line = self.stream.readline(readable)
self.remaining -= len(line)
return line

def readlines(self, hint=-1, /):
lines = []
read = 0
while line := self.readline():
lines.append(line)
read += len(line)
if hint > 0 and read >= hint:
break
return lines

def __iter__(self):
while line := self.readline():
yield line


class WSGIRequestHandler(BaseHTTPRequestHandler):

Expand Down Expand Up @@ -104,6 +140,13 @@ def get_environ(self):
env['HTTP_'+k] = v
return env

def get_stdin(self):
length = self.headers.get('content-length')
if length:
return InputWrapper(self.rfile, int(length))
else:
return self.rfile
Comment on lines +143 to +148
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to RFC 9110, Content-Length must be an integer number. Do we need to handle cases where it is not an integer as an exception?


def get_stderr(self):
return sys.stderr

Expand All @@ -122,8 +165,8 @@ def handle(self):
return

handler = ServerHandler(
self.rfile, self.wfile, self.get_stderr(), self.get_environ(),
multithread=False,
self.get_stdin(), self.wfile, self.get_stderr(),
self.get_environ(), multithread=False,
)
handler.request_handler = self # backpointer for logging
handler.run(self.server.get_app())
Expand Down
4 changes: 1 addition & 3 deletions Lib/wsgiref/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,6 @@

* That wsgi.input is used properly:

- .read() is called with exactly one argument

- That it returns a string

- That readline, readlines, and __iter__ return strings
Expand Down Expand Up @@ -194,7 +192,7 @@ def __init__(self, wsgi_input):
self.input = wsgi_input

def read(self, *args):
assert_(len(args) == 1)
assert_(len(args) <= 1)
v = self.input.read(*args)
assert_(type(v) is bytes)
return v
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix :mod:`wsgiref.simple_server` blocking when a WSGI application reads past
the request body from ``wsgi.input``. Reads are now limited to the number of
bytes declared by the ``Content-Length`` header and an end-of-file condition
is simulated once that limit is reached, as required by :pep:`3333`.
Loading