Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e508abc333 | ||
|
|
5003a5b3d9 | ||
|
|
4ed101dfc6 | ||
|
|
833fecb105 | ||
|
|
d527bdb7c3 | ||
|
|
2516b296a7 |
@@ -1,5 +1,11 @@
|
|||||||
# Microdot change log
|
# Microdot change log
|
||||||
|
|
||||||
|
**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**!)
|
||||||
|
- 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))
|
||||||
|
|
||||||
**Release 0.6.0** - 2021-08-11
|
**Release 0.6.0** - 2021-08-11
|
||||||
|
|
||||||
- Better handling of content types in form and json methods [#24](https://github.com/miguelgrinberg/microdot/issues/24) ([commit](https://github.com/miguelgrinberg/microdot/commit/da32f23e35f871470a40638e7000e84b0ff6d17f))
|
- Better handling of content types in form and json methods [#24](https://github.com/miguelgrinberg/microdot/issues/24) ([commit](https://github.com/miguelgrinberg/microdot/commit/da32f23e35f871470a40638e7000e84b0ff6d17f))
|
||||||
|
|||||||
9
SECURITY.md
Normal file
9
SECURITY.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
If you think you've found a vulnerability on this project, please send me (Miguel Grinberg) an email at miguel.grinberg@gmail.com with a description of the problem. I will personally review the issue and respond to you with next steps.
|
||||||
|
|
||||||
|
If the issue is highly sensitive, you are welcome to encrypt your message. Here is my [PGP key](https://keyserver.ubuntu.com/pks/lookup?search=miguel.grinberg%40gmail.com&fingerprint=on&op=index).
|
||||||
|
|
||||||
|
Please do not disclose vulnerabilities publicly before discussing how to proceed with me.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[metadata]
|
[metadata]
|
||||||
name = microdot
|
name = microdot
|
||||||
version = 0.6.0
|
version = 0.7.0
|
||||||
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
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ class Request():
|
|||||||
"""An HTTP request class.
|
"""An HTTP request class.
|
||||||
|
|
||||||
:var app: The application instance to which this request belongs.
|
:var app: The application instance to which this request belongs.
|
||||||
|
:var client_addr: The address of the client, as a tuple (host, port).
|
||||||
:var method: The HTTP method of the request.
|
:var method: The HTTP method of the request.
|
||||||
:var path: The path portion of the URL.
|
:var path: The path portion of the URL.
|
||||||
:var query_string: The query string portion of the URL.
|
:var query_string: The query string portion of the URL.
|
||||||
@@ -188,6 +189,15 @@ class Request():
|
|||||||
:var g: A general purpose container for applications to store data during
|
:var g: A general purpose container for applications to store data during
|
||||||
the life of the request.
|
the life of the request.
|
||||||
"""
|
"""
|
||||||
|
#: Specify the maximum payload size that is accepted. Requests with larger
|
||||||
|
#: payloads will be rejected with a 413 status code. Applications can
|
||||||
|
#: change this maximum as necessary.
|
||||||
|
#:
|
||||||
|
#: Example::
|
||||||
|
#:
|
||||||
|
#: Request.max_content_length = 1 * 1024 * 1024 # 1MB requests allowed
|
||||||
|
max_content_length = 16 * 1024
|
||||||
|
|
||||||
class G:
|
class G:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -254,7 +264,8 @@ class Request():
|
|||||||
content_length = int(value)
|
content_length = int(value)
|
||||||
|
|
||||||
# body
|
# body
|
||||||
body = client_stream.read(content_length) if content_length else b''
|
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)
|
body)
|
||||||
@@ -296,6 +307,9 @@ class Response():
|
|||||||
:param status_code: The numeric HTTP status code of the response. The
|
:param status_code: The numeric HTTP status code of the response. The
|
||||||
default is 200.
|
default is 200.
|
||||||
:param headers: A dictionary of headers to include in the response.
|
:param headers: A dictionary of headers to include in the response.
|
||||||
|
:param reason: A custom reason phrase to add after the status code. The
|
||||||
|
default is "OK" for responses with a 200 status code and
|
||||||
|
"N/A" for any other status codes.
|
||||||
"""
|
"""
|
||||||
types_map = {
|
types_map = {
|
||||||
'css': 'text/css',
|
'css': 'text/css',
|
||||||
@@ -766,39 +780,45 @@ class Microdot():
|
|||||||
|
|
||||||
req = Request.create(self, stream, addr)
|
req = Request.create(self, stream, addr)
|
||||||
if req:
|
if req:
|
||||||
f = self.find_route(req)
|
if req.content_length > req.max_content_length:
|
||||||
try:
|
if 413 in self.error_handlers:
|
||||||
res = None
|
res = self.error_handlers[413](req)
|
||||||
if f:
|
|
||||||
for handler in self.before_request_handlers:
|
|
||||||
res = handler(req)
|
|
||||||
if res:
|
|
||||||
break
|
|
||||||
if res is None:
|
|
||||||
res = f(req, **req.url_args)
|
|
||||||
if isinstance(res, tuple):
|
|
||||||
res = Response(*res)
|
|
||||||
elif not isinstance(res, Response):
|
|
||||||
res = Response(res)
|
|
||||||
for handler in self.after_request_handlers:
|
|
||||||
res = handler(req, res) or res
|
|
||||||
elif 404 in self.error_handlers:
|
|
||||||
res = self.error_handlers[404](req)
|
|
||||||
else:
|
else:
|
||||||
res = 'Not found', 404
|
res = 'Payload too large', 413
|
||||||
except Exception as exc:
|
else:
|
||||||
print_exception(exc)
|
f = self.find_route(req)
|
||||||
res = None
|
try:
|
||||||
if exc.__class__ in self.error_handlers:
|
res = None
|
||||||
try:
|
if f:
|
||||||
res = self.error_handlers[exc.__class__](req, exc)
|
for handler in self.before_request_handlers:
|
||||||
except Exception as exc2: # pragma: no cover
|
res = handler(req)
|
||||||
print_exception(exc2)
|
if res:
|
||||||
if res is None:
|
break
|
||||||
if 500 in self.error_handlers:
|
if res is None:
|
||||||
res = self.error_handlers[500](req)
|
res = f(req, **req.url_args)
|
||||||
|
if isinstance(res, tuple):
|
||||||
|
res = Response(*res)
|
||||||
|
elif not isinstance(res, Response):
|
||||||
|
res = Response(res)
|
||||||
|
for handler in self.after_request_handlers:
|
||||||
|
res = handler(req, res) or res
|
||||||
|
elif 404 in self.error_handlers:
|
||||||
|
res = self.error_handlers[404](req)
|
||||||
else:
|
else:
|
||||||
res = 'Internal server error', 500
|
res = 'Not found', 404
|
||||||
|
except Exception as exc:
|
||||||
|
print_exception(exc)
|
||||||
|
res = None
|
||||||
|
if exc.__class__ in self.error_handlers:
|
||||||
|
try:
|
||||||
|
res = self.error_handlers[exc.__class__](req, exc)
|
||||||
|
except Exception as exc2: # pragma: no cover
|
||||||
|
print_exception(exc2)
|
||||||
|
if res is None:
|
||||||
|
if 500 in self.error_handlers:
|
||||||
|
res = self.error_handlers[500](req)
|
||||||
|
else:
|
||||||
|
res = 'Internal server error', 500
|
||||||
if isinstance(res, tuple):
|
if isinstance(res, tuple):
|
||||||
res = Response(*res)
|
res = Response(*res)
|
||||||
elif not isinstance(res, Response):
|
elif not isinstance(res, Response):
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ class Request(BaseRequest):
|
|||||||
content_length = int(value)
|
content_length = int(value)
|
||||||
|
|
||||||
# body
|
# body
|
||||||
body = await client_stream.read(content_length) \
|
body = await client_stream.read(content_length) if content_length and \
|
||||||
if content_length else b''
|
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)
|
body)
|
||||||
@@ -69,6 +69,9 @@ class Response(BaseResponse):
|
|||||||
:param status_code: The numeric HTTP status code of the response. The
|
:param status_code: The numeric HTTP status code of the response. The
|
||||||
default is 200.
|
default is 200.
|
||||||
:param headers: A dictionary of headers to include in the response.
|
:param headers: A dictionary of headers to include in the response.
|
||||||
|
:param reason: A custom reason phrase to add after the status code. The
|
||||||
|
default is "OK" for responses with a 200 status code and
|
||||||
|
"N/A" for any other status codes.
|
||||||
"""
|
"""
|
||||||
async def write(self, stream):
|
async def write(self, stream):
|
||||||
self.complete()
|
self.complete()
|
||||||
@@ -210,44 +213,51 @@ class Microdot(BaseMicrodot):
|
|||||||
req = await Request.create(self, reader,
|
req = await Request.create(self, reader,
|
||||||
writer.get_extra_info('peername'))
|
writer.get_extra_info('peername'))
|
||||||
if req:
|
if req:
|
||||||
f = self.find_route(req)
|
if req.content_length > req.max_content_length:
|
||||||
try:
|
if 413 in self.error_handlers:
|
||||||
res = None
|
|
||||||
if f:
|
|
||||||
for handler in self.before_request_handlers:
|
|
||||||
res = await self._invoke_handler(handler, req)
|
|
||||||
if res:
|
|
||||||
break
|
|
||||||
if res is None:
|
|
||||||
res = await self._invoke_handler(
|
|
||||||
f, req, **req.url_args)
|
|
||||||
if isinstance(res, tuple):
|
|
||||||
res = Response(*res)
|
|
||||||
elif not isinstance(res, Response):
|
|
||||||
res = Response(res)
|
|
||||||
for handler in self.after_request_handlers:
|
|
||||||
res = await self._invoke_handler(
|
|
||||||
handler, req, res) or res
|
|
||||||
elif 404 in self.error_handlers:
|
|
||||||
res = await self._invoke_handler(
|
res = await self._invoke_handler(
|
||||||
self.error_handlers[404], req)
|
self.error_handlers[413], req)
|
||||||
else:
|
else:
|
||||||
res = 'Not found', 404
|
res = 'Payload too large', 413
|
||||||
except Exception as exc:
|
else:
|
||||||
print_exception(exc)
|
f = self.find_route(req)
|
||||||
res = None
|
try:
|
||||||
if exc.__class__ in self.error_handlers:
|
res = None
|
||||||
try:
|
if f:
|
||||||
|
for handler in self.before_request_handlers:
|
||||||
|
res = await self._invoke_handler(handler, req)
|
||||||
|
if res:
|
||||||
|
break
|
||||||
|
if res is None:
|
||||||
|
res = await self._invoke_handler(
|
||||||
|
f, req, **req.url_args)
|
||||||
|
if isinstance(res, tuple):
|
||||||
|
res = Response(*res)
|
||||||
|
elif not isinstance(res, Response):
|
||||||
|
res = Response(res)
|
||||||
|
for handler in self.after_request_handlers:
|
||||||
|
res = await self._invoke_handler(
|
||||||
|
handler, req, res) or res
|
||||||
|
elif 404 in self.error_handlers:
|
||||||
res = await self._invoke_handler(
|
res = await self._invoke_handler(
|
||||||
self.error_handlers[exc.__class__], req, exc)
|
self.error_handlers[404], req)
|
||||||
except Exception as exc2: # pragma: no cover
|
|
||||||
print_exception(exc2)
|
|
||||||
if res is None:
|
|
||||||
if 500 in self.error_handlers:
|
|
||||||
res = await self._invoke_handler(
|
|
||||||
self.error_handlers[500], req)
|
|
||||||
else:
|
else:
|
||||||
res = 'Internal server error', 500
|
res = 'Not found', 404
|
||||||
|
except Exception as exc:
|
||||||
|
print_exception(exc)
|
||||||
|
res = None
|
||||||
|
if exc.__class__ in self.error_handlers:
|
||||||
|
try:
|
||||||
|
res = await self._invoke_handler(
|
||||||
|
self.error_handlers[exc.__class__], req, exc)
|
||||||
|
except Exception as exc2: # pragma: no cover
|
||||||
|
print_exception(exc2)
|
||||||
|
if res is None:
|
||||||
|
if 500 in self.error_handlers:
|
||||||
|
res = await self._invoke_handler(
|
||||||
|
self.error_handlers[500], req)
|
||||||
|
else:
|
||||||
|
res = 'Internal server error', 500
|
||||||
if isinstance(res, tuple):
|
if isinstance(res, tuple):
|
||||||
res = Response(*res)
|
res = Response(*res)
|
||||||
elif not isinstance(res, Response):
|
elif not isinstance(res, Response):
|
||||||
|
|||||||
@@ -192,6 +192,42 @@ class TestMicrodot(unittest.TestCase):
|
|||||||
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
|
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
|
||||||
self.assertTrue(fd.response.endswith(b'\r\n\r\n404'))
|
self.assertTrue(fd.response.endswith(b'\r\n\r\n404'))
|
||||||
|
|
||||||
|
def test_413(self):
|
||||||
|
app = Microdot()
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index(req):
|
||||||
|
return 'foo'
|
||||||
|
|
||||||
|
mock_socket.clear_requests()
|
||||||
|
fd = mock_socket.add_request('GET', '/foo', body='x' * 17000)
|
||||||
|
self._add_shutdown(app)
|
||||||
|
app.run()
|
||||||
|
self.assertTrue(fd.response.startswith(b'HTTP/1.0 413 N/A\r\n'))
|
||||||
|
self.assertIn(b'Content-Length: 17\r\n', fd.response)
|
||||||
|
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
|
||||||
|
self.assertTrue(fd.response.endswith(b'\r\n\r\nPayload too large'))
|
||||||
|
|
||||||
|
def test_413_handler(self):
|
||||||
|
app = Microdot()
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index(req):
|
||||||
|
return 'foo'
|
||||||
|
|
||||||
|
@app.errorhandler(413)
|
||||||
|
def handle_413(req):
|
||||||
|
return '413', 400
|
||||||
|
|
||||||
|
mock_socket.clear_requests()
|
||||||
|
fd = mock_socket.add_request('GET', '/foo', body='x' * 17000)
|
||||||
|
self._add_shutdown(app)
|
||||||
|
app.run()
|
||||||
|
self.assertTrue(fd.response.startswith(b'HTTP/1.0 400 N/A\r\n'))
|
||||||
|
self.assertIn(b'Content-Length: 3\r\n', fd.response)
|
||||||
|
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
|
||||||
|
self.assertTrue(fd.response.endswith(b'\r\n\r\n413'))
|
||||||
|
|
||||||
def test_500(self):
|
def test_500(self):
|
||||||
app = Microdot()
|
app = Microdot()
|
||||||
|
|
||||||
|
|||||||
@@ -78,3 +78,15 @@ class TestRequest(unittest.TestCase):
|
|||||||
body='foo=bar&abc=def&x=%2f%%')
|
body='foo=bar&abc=def&x=%2f%%')
|
||||||
req = Request.create('app', fd, 'addr')
|
req = Request.create('app', fd, 'addr')
|
||||||
self.assertIsNone(req.form)
|
self.assertIsNone(req.form)
|
||||||
|
|
||||||
|
def test_large_payload(self):
|
||||||
|
saved_max_content_length = Request.max_content_length
|
||||||
|
Request.max_content_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')
|
||||||
|
assert req.body == b''
|
||||||
|
|
||||||
|
Request.max_content_length = saved_max_content_length
|
||||||
|
|||||||
@@ -170,6 +170,42 @@ class TestMicrodotAsync(unittest.TestCase):
|
|||||||
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
|
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
|
||||||
self.assertTrue(fd.response.endswith(b'\r\n\r\n404'))
|
self.assertTrue(fd.response.endswith(b'\r\n\r\n404'))
|
||||||
|
|
||||||
|
def test_413(self):
|
||||||
|
app = Microdot()
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index(req):
|
||||||
|
return 'foo'
|
||||||
|
|
||||||
|
mock_socket.clear_requests()
|
||||||
|
fd = mock_socket.add_request('GET', '/foo', body='x' * 17000)
|
||||||
|
self._add_shutdown(app)
|
||||||
|
app.run()
|
||||||
|
self.assertTrue(fd.response.startswith(b'HTTP/1.0 413 N/A\r\n'))
|
||||||
|
self.assertIn(b'Content-Length: 17\r\n', fd.response)
|
||||||
|
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
|
||||||
|
self.assertTrue(fd.response.endswith(b'\r\n\r\nPayload too large'))
|
||||||
|
|
||||||
|
def test_413_handler(self):
|
||||||
|
app = Microdot()
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index(req):
|
||||||
|
return 'foo'
|
||||||
|
|
||||||
|
@app.errorhandler(413)
|
||||||
|
async def handle_413(req):
|
||||||
|
return '413', 400
|
||||||
|
|
||||||
|
mock_socket.clear_requests()
|
||||||
|
fd = mock_socket.add_request('GET', '/foo', body='x' * 17000)
|
||||||
|
self._add_shutdown(app)
|
||||||
|
app.run()
|
||||||
|
self.assertTrue(fd.response.startswith(b'HTTP/1.0 400 N/A\r\n'))
|
||||||
|
self.assertIn(b'Content-Length: 3\r\n', fd.response)
|
||||||
|
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
|
||||||
|
self.assertTrue(fd.response.endswith(b'\r\n\r\n413'))
|
||||||
|
|
||||||
def test_500(self):
|
def test_500(self):
|
||||||
app = Microdot()
|
app = Microdot()
|
||||||
|
|
||||||
|
|||||||
@@ -88,3 +88,15 @@ class TestRequestAsync(unittest.TestCase):
|
|||||||
body='foo=bar&abc=def&x=%2f%%')
|
body='foo=bar&abc=def&x=%2f%%')
|
||||||
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):
|
||||||
|
saved_max_content_length = Request.max_content_length
|
||||||
|
Request.max_content_length = 16
|
||||||
|
|
||||||
|
fd = get_async_request_fd('GET', '/foo', headers={
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
|
body='foo=bar&abc=def&x=y')
|
||||||
|
req = _run(Request.create('app', fd, 'addr'))
|
||||||
|
assert req.body == b''
|
||||||
|
|
||||||
|
Request.max_content_length = saved_max_content_length
|
||||||
|
|||||||
Reference in New Issue
Block a user