9 Commits

Author SHA1 Message Date
Miguel Grinberg
453e133cc2 Release 0.8.1 2022-03-18 23:51:28 +00:00
Miguel Grinberg
29a9f6f46c Optimizations for request streams and bodies 2022-02-21 18:11:19 +01:00
Miguel Grinberg
9d3222ae4b Version 0.8.1.dev0 2022-02-18 17:41:16 +00:00
Miguel Grinberg
f23a6be2db Release 0.8.0 2022-02-18 17:40:59 +00:00
Miguel Grinberg
992fa722c1 Support streamed request payloads (Fixes #26) 2022-02-18 17:32:14 +00:00
Steve Li
e16fb94b2d Use case insensitive comparisons for HTTP headers (#33) 2022-01-31 12:10:23 +00:00
Miguel Grinberg
c130d8f2d4 simplified hello_async.py example 2022-01-22 23:23:31 +00:00
Miguel Grinberg
bd82c4deab More robust logic to read request body (Fixes #31) 2021-10-23 19:03:31 +01:00
Miguel Grinberg
7bc5d724f0 Version 0.7.3.dev0 2021-09-28 17:23:17 +01:00
10 changed files with 149 additions and 61 deletions

View File

@@ -1,5 +1,16 @@
# Microdot change log
**Release 0.8.1** - 2022-03-18
- Optimizations for request streams and bodies ([commit](https://github.com/miguelgrinberg/microdot/commit/29a9f6f46c737aa0fd452766c23bd83008594ac4))
**Release 0.8.0** - 2022-02-18
- Support streamed request payloads [#26](https://github.com/miguelgrinberg/microdot/issues/26) ([commit](https://github.com/miguelgrinberg/microdot/commit/992fa722c1312c0ac0ee9fbd5e23ad7b52d3caca))
- Use case insensitive comparisons for HTTP headers [#33](https://github.com/miguelgrinberg/microdot/issues/33) ([commit](https://github.com/miguelgrinberg/microdot/commit/e16fb94b2d1e88ef681d70f7f456c37ee9859df6)) (thanks **Steve Li**!)
- More robust logic to read request body [#31](https://github.com/miguelgrinberg/microdot/issues/31) ([commit](https://github.com/miguelgrinberg/microdot/commit/bd82c4deabf40d37e6b7397b08e8eb40ba2b6a42))
- Simplified `hello_async.py` example ([commit](https://github.com/miguelgrinberg/microdot/commit/c130d8f2d45dcce9606dda25d31d653ce91faf92))
**Release 0.7.2** - 2021-09-28
- Document a security risk in the send_file function ([commit](https://github.com/miguelgrinberg/microdot/commit/d29ed6aaa1f2080fcf471bf6ae0f480f95ff1716)) (thanks **Ky Tran**!)

View File

@@ -1,33 +0,0 @@
#!/bin/bash
VERSION=$1
if [[ "$VERSION" == "" ]]; then
echo Usage: $0 "<version>"
exit 1
fi
git diff --cached --exit-code >/dev/null
if [[ "$?" != "0" ]]; then
echo Commit your changes before using this script.
exit 1
fi
set -e
for PKG in microdot*; do
echo Building $PKG...
cd $PKG
sed -i "" "s/version.*$/version=\"$VERSION\",/" setup.py
git add setup.py
rm -rf dist
python setup.py sdist bdist_wheel --universal
cd ..
done
git commit -m "Release v$VERSION"
git tag v$VERSION
git push --tags origin master
for PKG in microdot*; do
echo Releasing $PKG...
cd $PKG
twine upload dist/*
cd ..
done

View File

@@ -33,8 +33,4 @@ async def shutdown(request):
return 'The server is shutting down...'
async def main():
await app.start_server(debug=True)
asyncio.run(main())
app.run(debug=True)

View File

@@ -1,6 +1,6 @@
[metadata]
name = microdot
version = 0.7.2
version = 0.8.1
author = Miguel Grinberg
author_email = miguel.grinberg@gmail.com
description = The impossibly small web framework for MicroPython

View File

@@ -181,7 +181,8 @@ class Request():
:var cookies: A dictionary with the cookies included in the request.
:var content_length: The parsed ``Content-Length`` header.
:var content_type: The parsed ``Content-Type`` header.
:var body: A stream from where the body can be read.
:var stream: The input stream, containing the request body.
:var body: The body of the request, as bytes.
:var json: The parsed JSON body, as a dictionary or list, or ``None`` if
the request does not have a JSON body.
:var form: The parsed form submission body, as a :class:`MultiDict` object,
@@ -198,6 +199,17 @@ class Request():
#: Request.max_content_length = 1 * 1024 * 1024 # 1MB requests allowed
max_content_length = 16 * 1024
#: Specify the maximum payload size that can be stored in ``body``.
#: Requests with payloads that are larger than this size and up to
#: ``max_content_length`` bytes will be accepted, but the application will
#: only be able to access the body of the request by reading from
#: ``stream``. Set to 0 if you always access the body as a stream.
#:
#: Example::
#:
#: Request.max_body_length = 4 * 1024 # up to 4KB bodies read
max_body_length = 16 * 1024
#: Specify the maximum length allowed for a line in the request. Requests
#: with longer lines will not be correctly interpreted. Applications can
#: change this maximum as necessary.
@@ -211,7 +223,7 @@ class Request():
pass
def __init__(self, app, client_addr, method, url, http_version, headers,
body):
body=None, stream=None):
self.app = app
self.client_addr = client_addr
self.method = method
@@ -228,15 +240,19 @@ class Request():
self.content_length = 0
self.content_type = None
for header, value in self.headers.items():
if header == 'Content-Length':
header = header.lower()
if header == 'content-length':
self.content_length = int(value)
elif header == 'Content-Type':
elif header == 'content-type':
self.content_type = value
elif header == 'Cookie':
elif header == 'cookie':
for cookie in value.split(';'):
name, value = cookie.strip().split('=', 1)
self.cookies[name] = value
self.body = body
self._body = body
self.body_used = False
self._stream = stream
self.stream_used = False
self._json = None
self._form = None
self.g = Request.G()
@@ -261,7 +277,6 @@ class Request():
# headers
headers = {}
content_length = 0
while True:
line = Request._safe_readline(client_stream).strip().decode()
if line == '':
@@ -269,15 +284,9 @@ class Request():
header, value = line.split(':', 1)
value = value.strip()
headers[header] = value
if header == 'Content-Length':
content_length = int(value)
# body
body = client_stream.read(content_length) if content_length and \
content_length <= Request.max_content_length else b''
return Request(app, client_addr, method, url, http_version, headers,
body)
stream=client_stream)
def _parse_urlencoded(self, urlencoded):
data = MultiDict()
@@ -285,6 +294,30 @@ class Request():
data[urldecode(k)] = urldecode(v)
return data
@property
def body(self):
if self.stream_used:
raise RuntimeError('Cannot use both stream and body')
if self._body is None:
self._body = b''
if self.content_length and \
self.content_length <= Request.max_body_length:
while len(self._body) < self.content_length:
data = self._stream.read(
self.content_length - len(self._body))
if len(data) == 0: # pragma: no cover
raise EOFError()
self._body += data
self.body_used = True
return self._body
@property
def stream(self):
if self.body_used:
raise RuntimeError('Cannot use both stream and body')
self.stream_used = True
return self._stream
@property
def json(self):
if self._json is None:

View File

@@ -10,6 +10,12 @@ try:
import uasyncio as asyncio
except ImportError:
import asyncio
try:
import uio as io
except ImportError:
import io
from microdot import Microdot as BaseMicrodot
from microdot import print_exception
from microdot import Request as BaseRequest
@@ -20,6 +26,23 @@ def _iscoroutine(coro):
return hasattr(coro, 'send') and hasattr(coro, 'throw')
class _AsyncBytesIO:
def __init__(self, data):
self.stream = io.BytesIO(data)
async def read(self, n=-1):
return self.stream.read(n)
async def readline(self): # pragma: no cover
return self.stream.readline()
async def readexactly(self, n): # pragma: no cover
return self.stream.read(n)
async def readuntil(self, separator=b'\n'): # pragma: no cover
return self.stream.readuntil(separator=separator)
class Request(BaseRequest):
@staticmethod
async def create(app, client_stream, client_addr):
@@ -51,15 +74,27 @@ class Request(BaseRequest):
header, value = line.split(':', 1)
value = value.strip()
headers[header] = value
if header == 'Content-Length':
if header.lower() == 'content-length':
content_length = int(value)
# body
body = await client_stream.read(content_length) if content_length and \
content_length <= Request.max_content_length else b''
body = b''
print(Request.max_body_length)
if content_length and content_length <= Request.max_body_length:
body = await client_stream.readexactly(content_length)
stream = None
else:
body = b''
stream = client_stream
return Request(app, client_addr, method, url, http_version, headers,
body)
body=body, stream=stream)
@property
def stream(self):
if self._stream is None:
self._stream = _AsyncBytesIO(self._body)
return self._stream
@staticmethod
async def _safe_readline(stream):

View File

@@ -91,14 +91,38 @@ class TestRequest(unittest.TestCase):
Request.max_readline = saved_max_readline
def test_stream(self):
fd = get_request_fd('GET', '/foo', headers={
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': '19'},
body='foo=bar&abc=def&x=y')
req = Request.create('app', fd, 'addr')
self.assertEqual(req.stream.read(), b'foo=bar&abc=def&x=y')
with self.assertRaises(RuntimeError):
req.body
def test_body(self):
fd = get_request_fd('GET', '/foo', headers={
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': '19'},
body='foo=bar&abc=def&x=y')
req = Request.create('app', fd, 'addr')
self.assertEqual(req.body, b'foo=bar&abc=def&x=y')
with self.assertRaises(RuntimeError):
req.stream
def test_large_payload(self):
saved_max_content_length = Request.max_content_length
Request.max_content_length = 16
saved_max_body_length = Request.max_body_length
Request.max_content_length = 32
Request.max_body_length = 16
fd = get_request_fd('GET', '/foo', headers={
'Content-Type': 'application/x-www-form-urlencoded'},
body='foo=bar&abc=def&x=y')
req = Request.create('app', fd, 'addr')
self.assertEqual(req.body, b'')
self.assertEqual(req.stream.read(), b'foo=bar&abc=def&x=y')
Request.max_content_length = saved_max_content_length
Request.max_body_length = saved_max_body_length

View File

@@ -167,6 +167,9 @@ class TestResponse(unittest.TestCase):
self.assertEqual(res.status_code, 301)
self.assertEqual(res.headers['Location'], '/foo')
with self.assertRaises(ValueError):
Response.redirect('/foo\x0d\x0a\x0d\x0a<p>Foo</p>')
def test_send_file(self):
files = [
('test.txt', 'text/plain'),

View File

@@ -101,14 +101,30 @@ class TestRequestAsync(unittest.TestCase):
Request.max_readline = saved_max_readline
def test_stream(self):
fd = get_async_request_fd('GET', '/foo', headers={
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': '19'},
body='foo=bar&abc=def&x=y')
req = _run(Request.create('app', fd, 'addr'))
self.assertEqual(req.body, b'foo=bar&abc=def&x=y')
data = _run(req.stream.read())
self.assertEqual(data, b'foo=bar&abc=def&x=y')
def test_large_payload(self):
saved_max_content_length = Request.max_content_length
Request.max_content_length = 16
saved_max_body_length = Request.max_body_length
Request.max_content_length = 32
Request.max_body_length = 16
fd = get_async_request_fd('GET', '/foo', headers={
'Content-Type': 'application/x-www-form-urlencoded'},
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': '19'},
body='foo=bar&abc=def&x=y')
req = _run(Request.create('app', fd, 'addr'))
self.assertEqual(req.body, b'')
data = _run(req.stream.read())
self.assertEqual(data, b'foo=bar&abc=def&x=y')
Request.max_content_length = saved_max_content_length
Request.max_body_length = saved_max_body_length

View File

@@ -56,7 +56,10 @@ class FakeStreamAsync:
async def readline(self):
return self.stream.readline()
async def read(self, n):
async def read(self, n=-1):
return self.stream.read(n)
async def readexactly(self, n):
return self.stream.read(n)
async def awrite(self, data):