Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
453e133cc2 | ||
|
|
29a9f6f46c | ||
|
|
9d3222ae4b | ||
|
|
f23a6be2db | ||
|
|
992fa722c1 | ||
|
|
e16fb94b2d | ||
|
|
c130d8f2d4 | ||
|
|
bd82c4deab | ||
|
|
7bc5d724f0 | ||
|
|
f23c78533e | ||
|
|
d29ed6aaa1 | ||
|
|
8e5fb92ff1 | ||
|
|
06015934b8 | ||
|
|
568cd51fd2 | ||
|
|
2fe9793389 | ||
|
|
de9c991a9a | ||
|
|
d75449eb32 |
23
CHANGES.md
23
CHANGES.md
@@ -1,8 +1,29 @@
|
|||||||
# Microdot change log
|
# 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**!)
|
||||||
|
- Validate redirect URLs ([commit](https://github.com/miguelgrinberg/microdot/commit/8e5fb92ff1ccd50972b0c1cb5a6c3bd5eb54d86b)) (thanks **Ky Tran**!)
|
||||||
|
- Return a 400 error when request object could not be created ([commit](https://github.com/miguelgrinberg/microdot/commit/06015934b834622d39f52b3e13d16bfee9dc8e5a))
|
||||||
|
|
||||||
|
**Release 0.7.1** - 2021-09-27
|
||||||
|
|
||||||
|
- Breaking change: Limit the size of each request line to 2KB. A different maximum can be set in `Request.max_readline`. ([commit](https://github.com/miguelgrinberg/microdot/commit/de9c991a9ab836d57d5c08bf4282f99f073b502a)) (thanks **Ky Tran**!)
|
||||||
|
|
||||||
**Release 0.7.0** - 2021-09-27
|
**Release 0.7.0** - 2021-09-27
|
||||||
|
|
||||||
- Breaking change: Limit the size of the request body to 16KB. A different maximum can be set in `Request.max_content_length`. ([commit](https://github.com/miguelgrinberg/microdot/commit/5003a5b3d948a7cf365857b419bebf6e388593a1)) (thanks **Ky Tran**!)
|
- Breaking change: Limit the size of the request body to 16KB. A different maximum can be set in `Request.max_content_length`. ([commit](https://github.com/miguelgrinberg/microdot/commit/5003a5b3d948a7cf365857b419bebf6e388593a1))
|
||||||
- Add documentation for `request.client_addr` [#27](https://github.com/miguelgrinberg/microdot/issues/27) ([commit](https://github.com/miguelgrinberg/microdot/commit/833fecb105ce456b95f1d2a6ea96dceca1075814)) (thanks **Mark Blakeney**!)
|
- Add documentation for `request.client_addr` [#27](https://github.com/miguelgrinberg/microdot/issues/27) ([commit](https://github.com/miguelgrinberg/microdot/commit/833fecb105ce456b95f1d2a6ea96dceca1075814)) (thanks **Mark Blakeney**!)
|
||||||
- Added documentation for reason argument in the Response object ([commit](https://github.com/miguelgrinberg/microdot/commit/d527bdb7c32ab918a1ecf6956cf3a9f544504354))
|
- Added documentation for reason argument in the Response object ([commit](https://github.com/miguelgrinberg/microdot/commit/d527bdb7c32ab918a1ecf6956cf3a9f544504354))
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -33,8 +33,4 @@ async def shutdown(request):
|
|||||||
return 'The server is shutting down...'
|
return 'The server is shutting down...'
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
app.run(debug=True)
|
||||||
await app.start_server(debug=True)
|
|
||||||
|
|
||||||
|
|
||||||
asyncio.run(main())
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[metadata]
|
[metadata]
|
||||||
name = microdot
|
name = microdot
|
||||||
version = 0.7.0
|
version = 0.8.1
|
||||||
author = Miguel Grinberg
|
author = Miguel Grinberg
|
||||||
author_email = miguel.grinberg@gmail.com
|
author_email = miguel.grinberg@gmail.com
|
||||||
description = The impossibly small web framework for MicroPython
|
description = The impossibly small web framework for MicroPython
|
||||||
|
|||||||
105
src/microdot.py
105
src/microdot.py
@@ -181,7 +181,8 @@ class Request():
|
|||||||
:var cookies: A dictionary with the cookies included in the request.
|
:var cookies: A dictionary with the cookies included in the request.
|
||||||
:var content_length: The parsed ``Content-Length`` header.
|
:var content_length: The parsed ``Content-Length`` header.
|
||||||
:var content_type: The parsed ``Content-Type`` 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
|
:var json: The parsed JSON body, as a dictionary or list, or ``None`` if
|
||||||
the request does not have a JSON body.
|
the request does not have a JSON body.
|
||||||
:var form: The parsed form submission body, as a :class:`MultiDict` object,
|
:var form: The parsed form submission body, as a :class:`MultiDict` object,
|
||||||
@@ -198,11 +199,31 @@ class Request():
|
|||||||
#: Request.max_content_length = 1 * 1024 * 1024 # 1MB requests allowed
|
#: Request.max_content_length = 1 * 1024 * 1024 # 1MB requests allowed
|
||||||
max_content_length = 16 * 1024
|
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.
|
||||||
|
#:
|
||||||
|
#: Example::
|
||||||
|
#:
|
||||||
|
#: Request.max_readline = 16 * 1024 # 16KB lines allowed
|
||||||
|
max_readline = 2 * 1024
|
||||||
|
|
||||||
class G:
|
class G:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def __init__(self, app, client_addr, method, url, http_version, headers,
|
def __init__(self, app, client_addr, method, url, http_version, headers,
|
||||||
body):
|
body=None, stream=None):
|
||||||
self.app = app
|
self.app = app
|
||||||
self.client_addr = client_addr
|
self.client_addr = client_addr
|
||||||
self.method = method
|
self.method = method
|
||||||
@@ -219,15 +240,19 @@ class Request():
|
|||||||
self.content_length = 0
|
self.content_length = 0
|
||||||
self.content_type = None
|
self.content_type = None
|
||||||
for header, value in self.headers.items():
|
for header, value in self.headers.items():
|
||||||
if header == 'Content-Length':
|
header = header.lower()
|
||||||
|
if header == 'content-length':
|
||||||
self.content_length = int(value)
|
self.content_length = int(value)
|
||||||
elif header == 'Content-Type':
|
elif header == 'content-type':
|
||||||
self.content_type = value
|
self.content_type = value
|
||||||
elif header == 'Cookie':
|
elif header == 'cookie':
|
||||||
for cookie in value.split(';'):
|
for cookie in value.split(';'):
|
||||||
name, value = cookie.strip().split('=', 1)
|
name, value = cookie.strip().split('=', 1)
|
||||||
self.cookies[name] = value
|
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._json = None
|
||||||
self._form = None
|
self._form = None
|
||||||
self.g = Request.G()
|
self.g = Request.G()
|
||||||
@@ -244,7 +269,7 @@ class Request():
|
|||||||
This method returns a newly created ``Request`` object.
|
This method returns a newly created ``Request`` object.
|
||||||
"""
|
"""
|
||||||
# request line
|
# request line
|
||||||
line = client_stream.readline().strip().decode()
|
line = Request._safe_readline(client_stream).strip().decode()
|
||||||
if not line:
|
if not line:
|
||||||
return None
|
return None
|
||||||
method, url, http_version = line.split()
|
method, url, http_version = line.split()
|
||||||
@@ -252,23 +277,16 @@ class Request():
|
|||||||
|
|
||||||
# headers
|
# headers
|
||||||
headers = {}
|
headers = {}
|
||||||
content_length = 0
|
|
||||||
while True:
|
while True:
|
||||||
line = client_stream.readline().strip().decode()
|
line = Request._safe_readline(client_stream).strip().decode()
|
||||||
if line == '':
|
if line == '':
|
||||||
break
|
break
|
||||||
header, value = line.split(':', 1)
|
header, value = line.split(':', 1)
|
||||||
value = value.strip()
|
value = value.strip()
|
||||||
headers[header] = value
|
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,
|
return Request(app, client_addr, method, url, http_version, headers,
|
||||||
body)
|
stream=client_stream)
|
||||||
|
|
||||||
def _parse_urlencoded(self, urlencoded):
|
def _parse_urlencoded(self, urlencoded):
|
||||||
data = MultiDict()
|
data = MultiDict()
|
||||||
@@ -276,6 +294,30 @@ class Request():
|
|||||||
data[urldecode(k)] = urldecode(v)
|
data[urldecode(k)] = urldecode(v)
|
||||||
return data
|
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
|
@property
|
||||||
def json(self):
|
def json(self):
|
||||||
if self._json is None:
|
if self._json is None:
|
||||||
@@ -298,6 +340,13 @@ class Request():
|
|||||||
self._form = self._parse_urlencoded(self.body.decode())
|
self._form = self._parse_urlencoded(self.body.decode())
|
||||||
return self._form
|
return self._form
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _safe_readline(stream):
|
||||||
|
line = stream.readline(Request.max_readline + 1)
|
||||||
|
if len(line) > Request.max_readline:
|
||||||
|
raise ValueError('line too long')
|
||||||
|
return line
|
||||||
|
|
||||||
|
|
||||||
class Response():
|
class Response():
|
||||||
"""An HTTP response class.
|
"""An HTTP response class.
|
||||||
@@ -414,6 +463,8 @@ class Response():
|
|||||||
:param status_code: The 3xx status code to use for the redirect. The
|
:param status_code: The 3xx status code to use for the redirect. The
|
||||||
default is 302.
|
default is 302.
|
||||||
"""
|
"""
|
||||||
|
if '\x0d' in location or '\x0a' in location:
|
||||||
|
raise ValueError('invalid redirect URL')
|
||||||
return cls(status_code=status_code, headers={'Location': location})
|
return cls(status_code=status_code, headers={'Location': location})
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -426,6 +477,10 @@ class Response():
|
|||||||
:param content_type: The ``Content-Type`` header to use in the
|
:param content_type: The ``Content-Type`` header to use in the
|
||||||
response. If omitted, it is generated
|
response. If omitted, it is generated
|
||||||
automatically from the file extension.
|
automatically from the file extension.
|
||||||
|
|
||||||
|
Security note: The filename is assumed to be trusted. Never pass
|
||||||
|
filenames provided by the user before validating and sanitizing them
|
||||||
|
first.
|
||||||
"""
|
"""
|
||||||
if content_type is None:
|
if content_type is None:
|
||||||
ext = filename.split('.')[-1]
|
ext = filename.split('.')[-1]
|
||||||
@@ -778,7 +833,11 @@ class Microdot():
|
|||||||
else:
|
else:
|
||||||
stream = sock
|
stream = sock
|
||||||
|
|
||||||
req = Request.create(self, stream, addr)
|
req = None
|
||||||
|
try:
|
||||||
|
req = Request.create(self, stream, addr)
|
||||||
|
except Exception as exc: # pragma: no cover
|
||||||
|
print_exception(exc)
|
||||||
if req:
|
if req:
|
||||||
if req.content_length > req.max_content_length:
|
if req.content_length > req.max_content_length:
|
||||||
if 413 in self.error_handlers:
|
if 413 in self.error_handlers:
|
||||||
@@ -819,11 +878,13 @@ class Microdot():
|
|||||||
res = self.error_handlers[500](req)
|
res = self.error_handlers[500](req)
|
||||||
else:
|
else:
|
||||||
res = 'Internal server error', 500
|
res = 'Internal server error', 500
|
||||||
if isinstance(res, tuple):
|
else:
|
||||||
res = Response(*res)
|
res = 'Bad request', 400
|
||||||
elif not isinstance(res, Response):
|
if isinstance(res, tuple):
|
||||||
res = Response(res)
|
res = Response(*res)
|
||||||
res.write(stream)
|
elif not isinstance(res, Response):
|
||||||
|
res = Response(res)
|
||||||
|
res.write(stream)
|
||||||
stream.close()
|
stream.close()
|
||||||
if stream != sock: # pragma: no cover
|
if stream != sock: # pragma: no cover
|
||||||
sock.close()
|
sock.close()
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ try:
|
|||||||
import uasyncio as asyncio
|
import uasyncio as asyncio
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
|
try:
|
||||||
|
import uio as io
|
||||||
|
except ImportError:
|
||||||
|
import io
|
||||||
|
|
||||||
from microdot import Microdot as BaseMicrodot
|
from microdot import Microdot as BaseMicrodot
|
||||||
from microdot import print_exception
|
from microdot import print_exception
|
||||||
from microdot import Request as BaseRequest
|
from microdot import Request as BaseRequest
|
||||||
@@ -20,6 +26,23 @@ def _iscoroutine(coro):
|
|||||||
return hasattr(coro, 'send') and hasattr(coro, 'throw')
|
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):
|
class Request(BaseRequest):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def create(app, client_stream, client_addr):
|
async def create(app, client_stream, client_addr):
|
||||||
@@ -34,7 +57,7 @@ class Request(BaseRequest):
|
|||||||
object.
|
object.
|
||||||
"""
|
"""
|
||||||
# request line
|
# request line
|
||||||
line = (await client_stream.readline()).strip().decode()
|
line = (await Request._safe_readline(client_stream)).strip().decode()
|
||||||
if not line: # pragma: no cover
|
if not line: # pragma: no cover
|
||||||
return None
|
return None
|
||||||
method, url, http_version = line.split()
|
method, url, http_version = line.split()
|
||||||
@@ -44,21 +67,41 @@ class Request(BaseRequest):
|
|||||||
headers = {}
|
headers = {}
|
||||||
content_length = 0
|
content_length = 0
|
||||||
while True:
|
while True:
|
||||||
line = (await client_stream.readline()).strip().decode()
|
line = (await Request._safe_readline(
|
||||||
|
client_stream)).strip().decode()
|
||||||
if line == '':
|
if line == '':
|
||||||
break
|
break
|
||||||
header, value = line.split(':', 1)
|
header, value = line.split(':', 1)
|
||||||
value = value.strip()
|
value = value.strip()
|
||||||
headers[header] = value
|
headers[header] = value
|
||||||
if header == 'Content-Length':
|
if header.lower() == 'content-length':
|
||||||
content_length = int(value)
|
content_length = int(value)
|
||||||
|
|
||||||
# body
|
# body
|
||||||
body = await client_stream.read(content_length) if content_length and \
|
body = b''
|
||||||
content_length <= Request.max_content_length else 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,
|
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):
|
||||||
|
line = (await stream.readline())
|
||||||
|
if len(line) > Request.max_readline:
|
||||||
|
raise ValueError('line too long')
|
||||||
|
return line
|
||||||
|
|
||||||
|
|
||||||
class Response(BaseResponse):
|
class Response(BaseResponse):
|
||||||
@@ -210,8 +253,12 @@ class Microdot(BaseMicrodot):
|
|||||||
self.server.close()
|
self.server.close()
|
||||||
|
|
||||||
async def dispatch_request(self, reader, writer):
|
async def dispatch_request(self, reader, writer):
|
||||||
req = await Request.create(self, reader,
|
req = None
|
||||||
writer.get_extra_info('peername'))
|
try:
|
||||||
|
req = await Request.create(self, reader,
|
||||||
|
writer.get_extra_info('peername'))
|
||||||
|
except Exception as exc: # pragma: no cover
|
||||||
|
print_exception(exc)
|
||||||
if req:
|
if req:
|
||||||
if req.content_length > req.max_content_length:
|
if req.content_length > req.max_content_length:
|
||||||
if 413 in self.error_handlers:
|
if 413 in self.error_handlers:
|
||||||
@@ -258,11 +305,13 @@ class Microdot(BaseMicrodot):
|
|||||||
self.error_handlers[500], req)
|
self.error_handlers[500], req)
|
||||||
else:
|
else:
|
||||||
res = 'Internal server error', 500
|
res = 'Internal server error', 500
|
||||||
if isinstance(res, tuple):
|
else:
|
||||||
res = Response(*res)
|
res = 'Bad request', 400
|
||||||
elif not isinstance(res, Response):
|
if isinstance(res, tuple):
|
||||||
res = Response(res)
|
res = Response(*res)
|
||||||
await res.write(writer)
|
elif not isinstance(res, Response):
|
||||||
|
res = Response(res)
|
||||||
|
await res.write(writer)
|
||||||
await writer.aclose()
|
await writer.aclose()
|
||||||
if self.debug and req: # pragma: no cover
|
if self.debug and req: # pragma: no cover
|
||||||
print('{method} {path} {status_code}'.format(
|
print('{method} {path} {status_code}'.format(
|
||||||
|
|||||||
@@ -73,7 +73,10 @@ class TestMicrodot(unittest.TestCase):
|
|||||||
mock_socket._requests.append(fd)
|
mock_socket._requests.append(fd)
|
||||||
self._add_shutdown(app)
|
self._add_shutdown(app)
|
||||||
app.run()
|
app.run()
|
||||||
assert fd.response == b''
|
self.assertTrue(fd.response.startswith(b'HTTP/1.0 400 N/A\r\n'))
|
||||||
|
self.assertIn(b'Content-Length: 11\r\n', fd.response)
|
||||||
|
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
|
||||||
|
self.assertTrue(fd.response.endswith(b'\r\n\r\nBad request'))
|
||||||
|
|
||||||
def test_method_decorators(self):
|
def test_method_decorators(self):
|
||||||
app = Microdot()
|
app = Microdot()
|
||||||
|
|||||||
@@ -79,14 +79,50 @@ class TestRequest(unittest.TestCase):
|
|||||||
req = Request.create('app', fd, 'addr')
|
req = Request.create('app', fd, 'addr')
|
||||||
self.assertIsNone(req.form)
|
self.assertIsNone(req.form)
|
||||||
|
|
||||||
|
def test_large_line(self):
|
||||||
|
saved_max_readline = Request.max_readline
|
||||||
|
Request.max_readline = 16
|
||||||
|
|
||||||
|
fd = get_request_fd('GET', '/foo', headers={
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
|
body='foo=bar&abc=def&x=y')
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
Request.create('app', fd, 'addr')
|
||||||
|
|
||||||
|
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):
|
def test_large_payload(self):
|
||||||
saved_max_content_length = Request.max_content_length
|
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={
|
fd = get_request_fd('GET', '/foo', headers={
|
||||||
'Content-Type': 'application/x-www-form-urlencoded'},
|
'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
body='foo=bar&abc=def&x=y')
|
body='foo=bar&abc=def&x=y')
|
||||||
req = Request.create('app', fd, 'addr')
|
req = Request.create('app', fd, 'addr')
|
||||||
assert req.body == b''
|
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_content_length = saved_max_content_length
|
||||||
|
Request.max_body_length = saved_max_body_length
|
||||||
|
|||||||
@@ -167,6 +167,9 @@ class TestResponse(unittest.TestCase):
|
|||||||
self.assertEqual(res.status_code, 301)
|
self.assertEqual(res.status_code, 301)
|
||||||
self.assertEqual(res.headers['Location'], '/foo')
|
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):
|
def test_send_file(self):
|
||||||
files = [
|
files = [
|
||||||
('test.txt', 'text/plain'),
|
('test.txt', 'text/plain'),
|
||||||
|
|||||||
@@ -84,7 +84,10 @@ class TestMicrodotAsync(unittest.TestCase):
|
|||||||
mock_socket._requests.append(fd)
|
mock_socket._requests.append(fd)
|
||||||
self._add_shutdown(app)
|
self._add_shutdown(app)
|
||||||
app.run()
|
app.run()
|
||||||
assert fd.response == b''
|
self.assertTrue(fd.response.startswith(b'HTTP/1.0 400 N/A\r\n'))
|
||||||
|
self.assertIn(b'Content-Length: 11\r\n', fd.response)
|
||||||
|
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
|
||||||
|
self.assertTrue(fd.response.endswith(b'\r\n\r\nBad request'))
|
||||||
|
|
||||||
def test_before_after_request(self):
|
def test_before_after_request(self):
|
||||||
app = Microdot()
|
app = Microdot()
|
||||||
|
|||||||
@@ -89,14 +89,42 @@ class TestRequestAsync(unittest.TestCase):
|
|||||||
req = _run(Request.create('app', fd, 'addr'))
|
req = _run(Request.create('app', fd, 'addr'))
|
||||||
self.assertIsNone(req.form)
|
self.assertIsNone(req.form)
|
||||||
|
|
||||||
def test_large_payload(self):
|
def test_large_line(self):
|
||||||
saved_max_content_length = Request.max_content_length
|
saved_max_readline = Request.max_readline
|
||||||
Request.max_content_length = 16
|
Request.max_readline = 16
|
||||||
|
|
||||||
fd = get_async_request_fd('GET', '/foo', headers={
|
fd = get_async_request_fd('GET', '/foo', headers={
|
||||||
'Content-Type': 'application/x-www-form-urlencoded'},
|
'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
body='foo=bar&abc=def&x=y')
|
body='foo=bar&abc=def&x=y')
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
_run(Request.create('app', fd, 'addr'))
|
||||||
|
|
||||||
|
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'))
|
req = _run(Request.create('app', fd, 'addr'))
|
||||||
assert req.body == b''
|
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
|
||||||
|
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-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_content_length = saved_max_content_length
|
||||||
|
Request.max_body_length = saved_max_body_length
|
||||||
|
|||||||
@@ -56,7 +56,10 @@ class FakeStreamAsync:
|
|||||||
async def readline(self):
|
async def readline(self):
|
||||||
return self.stream.readline()
|
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)
|
return self.stream.read(n)
|
||||||
|
|
||||||
async def awrite(self, data):
|
async def awrite(self, data):
|
||||||
|
|||||||
Reference in New Issue
Block a user