15 Commits

Author SHA1 Message Date
Miguel Grinberg
51a0aa62e1 Release 1.1.0 2022-09-17 23:17:38 +01:00
Miguel Grinberg
dc7a041ebd Recover from errors writing the response 2022-09-17 23:11:19 +01:00
Miguel Grinberg
59453a52a1 unit test fixes #nolog 2022-09-17 20:46:11 +01:00
Miguel Grinberg
75725795b4 Charset handling in Content-Type headers (Fixes #60) 2022-09-17 19:34:34 +01:00
Miguel Grinberg
019eb4d6bb Update README.md 2022-09-12 16:53:22 +01:00
Miguel Grinberg
fe750feb03 TLS fixes for WebSocket under MicroPython 2022-09-08 23:21:43 +01:00
Miguel Grinberg
b61f51f243 SSL/TLS Support 2022-09-05 10:27:59 +01:00
Miguel Grinberg
2399c29c8a Websocket standard and asyncio extensions (#55) 2022-09-03 20:04:34 +01:00
Sterling G. Baird
ec0f9ba855 Fix links to hello and gpio examples in documentation (#53) 2022-08-27 14:40:41 +01:00
Miguel Grinberg
a01fc9c3f0 Reorganized examples into subdirectories 2022-08-14 16:35:17 +01:00
Miguel Grinberg
3c125c43d2 Add abort function 2022-08-09 23:53:44 +01:00
Miguel Grinberg
e767426228 Update micropython libraries 2022-08-08 18:20:50 +01:00
Miguel Grinberg
42b6d69793 Update micropython tests to use release 1.19 2022-08-07 16:40:25 +01:00
Miguel Grinberg
2dc34a463b updated links to micropython libraries #nolog 2022-08-07 15:55:42 +01:00
Miguel Grinberg
abb7900691 Version 1.0.1.dev0 2022-08-07 15:53:29 +01:00
73 changed files with 1799 additions and 1147 deletions

View File

@@ -1,2 +1,5 @@
[run]
omit=src/utemplate/*
omit=
src/microdot_websocket_alt.py
src/microdot_asgi_websocket.py
src/microdot_ssl.py

View File

@@ -1,5 +1,17 @@
# Microdot change log
**Release 1.1.0** - 2022-09-17
- Websocket support [#55](https://github.com/miguelgrinberg/microdot/issues/55) ([commit](https://github.com/miguelgrinberg/microdot/commit/2399c29c8a45289f009f47fd66438452da93cdab))
- SSL/TLS support ([commit #1](https://github.com/miguelgrinberg/microdot/commit/b61f51f2434465b7a0ee197aabf46e8f99f6e8ad) [commit #2](https://github.com/miguelgrinberg/microdot/commit/fe750feb0373b41cb022521a6a3edf1973847a74))
- Add `abort()` function ([commit](https://github.com/miguelgrinberg/microdot/commit/3c125c43d2e037ce64138e22c1ff4186ea107471))
- Charset handling in Content-Type headers [#60](https://github.com/miguelgrinberg/microdot/issues/60) ([commit](https://github.com/miguelgrinberg/microdot/commit/75725795b45d275deaee133204e400e8fbb3de70))
- Recover from errors writing the response ([commit](https://github.com/miguelgrinberg/microdot/commit/dc7a041ebd30f38b9f6b22c4bbcd61993c43944e))
- Reorganized examples into subdirectories ([commit](https://github.com/miguelgrinberg/microdot/commit/a01fc9c3f070e21e705b8f12ceb8288b0f304569))
- Update tests to use MicroPython 1.19 ([commit](https://github.com/miguelgrinberg/microdot/commit/42b6d6979381d9cd8ccc6ab6e079f12ec5987b80))
- Update MicroPython libraries used by tests ([commit](https://github.com/miguelgrinberg/microdot/commit/e767426228eeacd58886bccb5046049e994c0479))
- Fix links to hello and gpio examples in documentation [#53](https://github.com/miguelgrinberg/microdot/issues/53) ([commit](https://github.com/miguelgrinberg/microdot/commit/ec0f9ba855cca7dd35cddad40c4cb7eb17d8842a)) (thanks **Sterling G. Baird**!)
**Release 1.0.0** - 2022-08-07
- User sessions with signed JWTs ([commit](https://github.com/miguelgrinberg/microdot/commit/355ffefcb2697b30d03359d35283835901f375d6))

Binary file not shown.

View File

@@ -152,10 +152,8 @@ Maintaing Secure User Sessions
* - Required external dependencies
- | CPython: `PyJWT <https://pyjwt.readthedocs.io/>`_
| MicroPython: `jwt.py <https://github.com/miguelgrinberg/micropython-lib/blob/ujwt-module/python-ecosys/ujwt/ujwt.py>`_,
`hmac <https://github.com/micropython/micropython-lib/blob/master/python-stdlib/hmac/hmac.py>`_,
`hashlib <https://github.com/miguelgrinberg/micropython-lib/blob/ujwt-module/python-stdlib/hashlib>`_,
`warnings <https://github.com/micropython/micropython-lib/blob/master/python-stdlib/warnings/warnings.py>`_
| MicroPython: `jwt.py <https://github.com/micropython/micropython-lib/blob/master/python-ecosys/pyjwt/jwt.py>`_,
`hmac <https://github.com/micropython/micropython-lib/blob/master/python-stdlib/hmac/hmac.py>`_
* - Examples
- | `login.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/login.py>`_
@@ -210,6 +208,125 @@ Example::
delete_session(req)
return redirect('/')
WebSocket Support
~~~~~~~~~~~~~~~~~
.. list-table::
:align: left
* - Compatibility
- | CPython & MicroPython
* - Required Microdot source files
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
| `microdot_websocket.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_websocket.py>`_
* - Required external dependencies
- | None
* - Examples
- | `echo.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/websocket/echo.py>`_
| `echo_wsgi.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/websocket/echo_wsgi.py>`_
The WebSocket extension provides a way for the application to handle WebSocket
requests. The :func:`websocket <microdot_websocket.with_websocket>` decorator
is used to mark a route handler as a WebSocket handler. The handler receives
a WebSocket object as a second argument. The WebSocket object provides
``send()`` and ``receive()`` methods to send and receive messages respectively.
Example::
@app.route('/echo')
@with_websocket
def echo(request, ws):
while True:
message = ws.receive()
ws.send(message)
.. note::
An unsupported *microsoft_websocket_alt.py* module, with the same
interface, is also provided. This module uses the native WebSocket support
in MicroPython that powers the WebREPL, and may provide slightly better
performance for MicroPython low-end boards. This module is not compatible
with CPython.
Asynchronous WebSocket
~~~~~~~~~~~~~~~~~~~~~~
.. list-table::
:align: left
* - Compatibility
- | CPython & MicroPython
* - Required Microdot source files
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
| `microdot_asyncio.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_asyncio.py>`_
| `microdot_websocket.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_websocket.py>`_
| `microdot_asyncio_websocket.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_asyncio_websocket.py>`_
* - Required external dependencies
- | CPython: None
| MicroPython: `uasyncio <https://github.com/micropython/micropython/tree/master/extmod/uasyncio>`_
* - Examples
- | `echo_async.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/websocket/echo_async.py>`_
This extension has the same interface as the synchronous WebSocket extension,
but the ``receive()`` and ``send()`` methods are asynchronous.
.. note::
An unsupported *microsoft_asgi_websocket.py* module, with the same
interface, is also provided. This module must be used instead of
*microsoft_asyncio_websocket.py* when the ASGI support is used. The
`echo_asgi.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/websocket/echo_asgi.py>`_
example shows how to use this module.
HTTPS Support
~~~~~~~~~~~~~
.. list-table::
:align: left
* - Compatibility
- | CPython & MicroPython
* - Required Microdot source files
- | `microdot.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot.py>`_
| `microdot_ssl.py <https://github.com/miguelgrinberg/microdot/tree/main/src/microdot_ssl.py>`_
* - Examples
- | `hello_tls.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/tls/hello_tls.py>`_
| `hello_asyncio_tls.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/tls/hello_asyncio_tls.py>`_
The ``run()`` function accepts an optional ``ssl`` argument, through which an
initialized ``SSLContext`` object can be passed. MicroPython does not currently
have a ``SSLContext`` implementation, so the ``microdot_ssl`` module provides
a basic implementation that can be used to create a context.
Example::
from microdot import Microdot
from microdot_ssl import create_ssl_context
app = Microdot()
@app.route('/')
def index(req):
return 'Hello, World!'
sslctx = create_ssl_context('cert.der', 'key.der')
app.run(port=4443, debug=True, ssl=sslctx)
.. note::
The ``microdot_ssl`` module is only needed for MicroPython. When used under
CPython, this module creates a standard ``SSLContext`` instance.
.. note::
The ``uasyncio`` library for MicroPython does not currently support TLS, so
this feature is not available for asynchronous applications on that
platform. The ``asyncio`` library for CPython is fully supported.
Test Client
~~~~~~~~~~~

View File

@@ -68,7 +68,7 @@ Running with CPython
- | None
* - Examples
- | `hello.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello.py>`_
- | `hello.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello/hello.py>`_
When using CPython, you can start the web server by running the script that
defines and runs the application instance::
@@ -93,8 +93,8 @@ Running with MicroPython
- | None
* - Examples
- | `hello.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello.py>`_
| `gpio.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/gpio.py>`_
- | `hello.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/hello/hello.py>`_
| `gpio.py <https://github.com/miguelgrinberg/microdot/blob/main/examples/gpio/gpio.py>`_
When using MicroPython, you can upload a *main.py* file containing the web
server code to your device along with *microdot.py*. MicroPython will

2
examples/hello/README.md Normal file
View File

@@ -0,0 +1,2 @@
This directory contains several "Hello, World!" type examples for different
platforms and configurations supported by Microdot.

View File

@@ -0,0 +1 @@
This directory contains examples that take advantage of user sessions.

View File

@@ -0,0 +1,2 @@
The example in this directory demonstrates how to serve static files out of a
directory.

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -0,0 +1 @@
This directory contain examples that demonstrate how to use streaming responses.

20
examples/tls/README.md Normal file
View File

@@ -0,0 +1,20 @@
This directory contains examples that demonstrate how to start TLS servers.
To run these examples, SSL certificate and private key files need to be
created. When running under CPython, the files should be in PEM format, named
`cert.pem` and `key.pem`. When running under MicroPython, they should be in DER
format, and named `cert.der` and `key.der`.
To quickly create a self-signed SSL certificate, use the following command:
```bash
openssl req -x509 -newkey rsa:4096 -nodes -out cert.pem -keyout key.pem -days 365
```
To convert the resulting PEM files to DER format for MicroPython, use these
commands:
```bash
openssl x509 -in cert.pem -out cert.der -outform DER
openssl rsa -in key.pem -out key.der -outform DER
```

24
examples/tls/echo_tls.py Normal file
View File

@@ -0,0 +1,24 @@
import sys
from microdot import Microdot, send_file
from microdot_websocket import with_websocket
from microdot_ssl import create_ssl_context
app = Microdot()
@app.route('/')
def index(request):
return send_file('index.html')
@app.route('/echo')
@with_websocket
def echo(request, ws):
while True:
data = ws.receive()
ws.send(data)
ext = 'der' if sys.implementation.name == 'micropython' else 'pem'
sslctx = create_ssl_context('cert.' + ext, 'key.' + ext)
app.run(port=4443, debug=True, ssl=sslctx)

View File

@@ -0,0 +1,35 @@
import ssl
from microdot_asyncio import Microdot
app = Microdot()
htmldoc = '''<!DOCTYPE html>
<html>
<head>
<title>Microdot Example Page</title>
</head>
<body>
<div>
<h1>Microdot Example Page</h1>
<p>Hello from Microdot!</p>
<p><a href="/shutdown">Click to shutdown the server</a></p>
</div>
</body>
</html>
'''
@app.route('/')
async def hello(request):
return htmldoc, 200, {'Content-Type': 'text/html'}
@app.route('/shutdown')
async def shutdown(request):
request.app.shutdown()
return 'The server is shutting down...'
sslctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
sslctx.load_cert_chain('cert.pem', 'key.pem')
app.run(port=4443, debug=True, ssl=sslctx)

36
examples/tls/hello_tls.py Normal file
View File

@@ -0,0 +1,36 @@
import sys
from microdot import Microdot
from microdot_ssl import create_ssl_context
app = Microdot()
htmldoc = '''<!DOCTYPE html>
<html>
<head>
<title>Microdot Example Page</title>
</head>
<body>
<div>
<h1>Microdot Example Page</h1>
<p>Hello from Microdot!</p>
<p><a href="/shutdown">Click to shutdown the server</a></p>
</div>
</body>
</html>
'''
@app.route('/')
def hello(request):
return htmldoc, 200, {'Content-Type': 'text/html'}
@app.route('/shutdown')
def shutdown(request):
request.app.shutdown()
return 'The server is shutting down...'
ext = 'der' if sys.implementation.name == 'micropython' else 'pem'
sslctx = create_ssl_context('cert.' + ext, 'key.' + ext)
app.run(port=4443, debug=True, ssl=sslctx)

35
examples/tls/index.html Normal file
View File

@@ -0,0 +1,35 @@
<!doctype html>
<html>
<head>
<title>Microdot TLS WebSocket Demo</title>
</head>
<body>
<h1>Microdot TLS WebSocket Demo</h1>
<div id="log"></div>
<br>
<form id="form">
<label for="text">Input: </label>
<input type="text" id="text" autofocus>
</form>
<script>
const log = (text, color) => {
document.getElementById('log').innerHTML += `<span style="color: ${color}">${text}</span><br>`;
};
const socket = new WebSocket('wss://' + location.host + '/echo');
socket.addEventListener('message', ev => {
log('<<< ' + ev.data, 'blue');
});
socket.addEventListener('close', ev => {
log('<<< closed');
});
document.getElementById('form').onsubmit = ev => {
ev.preventDefault();
const textField = document.getElementById('text');
log('>>> ' + textField.value, 'red');
socket.send(textField.value);
textField.value = '';
};
</script>
</body>
</html>

View File

@@ -0,0 +1 @@
This directory contains WebSocket examples.

View File

@@ -0,0 +1,20 @@
from microdot import Microdot, send_file
from microdot_websocket import with_websocket
app = Microdot()
@app.route('/')
def index(request):
return send_file('index.html')
@app.route('/echo')
@with_websocket
def echo(request, ws):
while True:
data = ws.receive()
ws.send(data)
app.run()

View File

@@ -0,0 +1,17 @@
from microdot_asgi import Microdot, send_file
from microdot_asgi_websocket import with_websocket
app = Microdot()
@app.route('/')
def index(request):
return send_file('index.html')
@app.route('/echo')
@with_websocket
async def echo(request, ws):
while True:
data = await ws.receive()
await ws.send(data)

View File

@@ -0,0 +1,20 @@
from microdot_asyncio import Microdot, send_file
from microdot_asyncio_websocket import with_websocket
app = Microdot()
@app.route('/')
def index(request):
return send_file('index.html')
@app.route('/echo')
@with_websocket
async def echo(request, ws):
while True:
data = await ws.receive()
await ws.send(data)
app.run()

View File

@@ -0,0 +1,17 @@
from microdot_wsgi import Microdot, send_file
from microdot_websocket import with_websocket
app = Microdot()
@app.route('/')
def index(request):
return send_file('index.html')
@app.route('/echo')
@with_websocket
def echo(request, ws):
while True:
data = ws.receive()
ws.send(data)

View File

@@ -0,0 +1,35 @@
<!doctype html>
<html>
<head>
<title>Microdot WebSocket Demo</title>
</head>
<body>
<h1>Microdot WebSocket Demo</h1>
<div id="log"></div>
<br>
<form id="form">
<label for="text">Input: </label>
<input type="text" id="text" autofocus>
</form>
<script>
const log = (text, color) => {
document.getElementById('log').innerHTML += `<span style="color: ${color}">${text}</span><br>`;
};
const socket = new WebSocket('ws://' + location.host + '/echo');
socket.addEventListener('message', ev => {
log('<<< ' + ev.data, 'blue');
});
socket.addEventListener('close', ev => {
log('<<< closed');
});
document.getElementById('form').onsubmit = ev => {
ev.preventDefault();
const textField = document.getElementById('text');
log('>>> ' + textField.value, 'red');
socket.send(textField.value);
textField.value = '';
};
</script>
</body>
</html>

View File

@@ -1,25 +0,0 @@
try:
import uhashlib
except ImportError:
uhashlib = None
def init():
for i in ("sha1", "sha224", "sha256", "sha384", "sha512"):
try:
c = __import__("_" + i, None, None, (), 1)
except ImportError:
c = uhashlib
c = getattr(c, i, None)
globals()[i] = c
init()
def new(algo, data=b""):
try:
c = globals()[algo]
return c(data)
except KeyError:
raise ValueError(algo)

View File

@@ -1 +0,0 @@
from ._sha256 import sha224

View File

@@ -1,301 +0,0 @@
SHA_BLOCKSIZE = 64
SHA_DIGESTSIZE = 32
def new_shaobject():
return {
"digest": [0] * 8,
"count_lo": 0,
"count_hi": 0,
"data": [0] * SHA_BLOCKSIZE,
"local": 0,
"digestsize": 0,
}
ROR = lambda x, y: (((x & 0xFFFFFFFF) >> (y & 31)) | (x << (32 - (y & 31)))) & 0xFFFFFFFF
Ch = lambda x, y, z: (z ^ (x & (y ^ z)))
Maj = lambda x, y, z: (((x | y) & z) | (x & y))
S = lambda x, n: ROR(x, n)
R = lambda x, n: (x & 0xFFFFFFFF) >> n
Sigma0 = lambda x: (S(x, 2) ^ S(x, 13) ^ S(x, 22))
Sigma1 = lambda x: (S(x, 6) ^ S(x, 11) ^ S(x, 25))
Gamma0 = lambda x: (S(x, 7) ^ S(x, 18) ^ R(x, 3))
Gamma1 = lambda x: (S(x, 17) ^ S(x, 19) ^ R(x, 10))
def sha_transform(sha_info):
W = []
d = sha_info["data"]
for i in range(0, 16):
W.append((d[4 * i] << 24) + (d[4 * i + 1] << 16) + (d[4 * i + 2] << 8) + d[4 * i + 3])
for i in range(16, 64):
W.append((Gamma1(W[i - 2]) + W[i - 7] + Gamma0(W[i - 15]) + W[i - 16]) & 0xFFFFFFFF)
ss = sha_info["digest"][:]
def RND(a, b, c, d, e, f, g, h, i, ki):
t0 = h + Sigma1(e) + Ch(e, f, g) + ki + W[i]
t1 = Sigma0(a) + Maj(a, b, c)
d += t0
h = t0 + t1
return d & 0xFFFFFFFF, h & 0xFFFFFFFF
ss[3], ss[7] = RND(ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 0, 0x428A2F98)
ss[2], ss[6] = RND(ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 1, 0x71374491)
ss[1], ss[5] = RND(ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 2, 0xB5C0FBCF)
ss[0], ss[4] = RND(ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 3, 0xE9B5DBA5)
ss[7], ss[3] = RND(ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 4, 0x3956C25B)
ss[6], ss[2] = RND(ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 5, 0x59F111F1)
ss[5], ss[1] = RND(ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 6, 0x923F82A4)
ss[4], ss[0] = RND(ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 7, 0xAB1C5ED5)
ss[3], ss[7] = RND(ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 8, 0xD807AA98)
ss[2], ss[6] = RND(ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 9, 0x12835B01)
ss[1], ss[5] = RND(ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 10, 0x243185BE)
ss[0], ss[4] = RND(ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 11, 0x550C7DC3)
ss[7], ss[3] = RND(ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 12, 0x72BE5D74)
ss[6], ss[2] = RND(ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 13, 0x80DEB1FE)
ss[5], ss[1] = RND(ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 14, 0x9BDC06A7)
ss[4], ss[0] = RND(ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 15, 0xC19BF174)
ss[3], ss[7] = RND(ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 16, 0xE49B69C1)
ss[2], ss[6] = RND(ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 17, 0xEFBE4786)
ss[1], ss[5] = RND(ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 18, 0x0FC19DC6)
ss[0], ss[4] = RND(ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 19, 0x240CA1CC)
ss[7], ss[3] = RND(ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 20, 0x2DE92C6F)
ss[6], ss[2] = RND(ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 21, 0x4A7484AA)
ss[5], ss[1] = RND(ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 22, 0x5CB0A9DC)
ss[4], ss[0] = RND(ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 23, 0x76F988DA)
ss[3], ss[7] = RND(ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 24, 0x983E5152)
ss[2], ss[6] = RND(ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 25, 0xA831C66D)
ss[1], ss[5] = RND(ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 26, 0xB00327C8)
ss[0], ss[4] = RND(ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 27, 0xBF597FC7)
ss[7], ss[3] = RND(ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 28, 0xC6E00BF3)
ss[6], ss[2] = RND(ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 29, 0xD5A79147)
ss[5], ss[1] = RND(ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 30, 0x06CA6351)
ss[4], ss[0] = RND(ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 31, 0x14292967)
ss[3], ss[7] = RND(ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 32, 0x27B70A85)
ss[2], ss[6] = RND(ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 33, 0x2E1B2138)
ss[1], ss[5] = RND(ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 34, 0x4D2C6DFC)
ss[0], ss[4] = RND(ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 35, 0x53380D13)
ss[7], ss[3] = RND(ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 36, 0x650A7354)
ss[6], ss[2] = RND(ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 37, 0x766A0ABB)
ss[5], ss[1] = RND(ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 38, 0x81C2C92E)
ss[4], ss[0] = RND(ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 39, 0x92722C85)
ss[3], ss[7] = RND(ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 40, 0xA2BFE8A1)
ss[2], ss[6] = RND(ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 41, 0xA81A664B)
ss[1], ss[5] = RND(ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 42, 0xC24B8B70)
ss[0], ss[4] = RND(ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 43, 0xC76C51A3)
ss[7], ss[3] = RND(ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 44, 0xD192E819)
ss[6], ss[2] = RND(ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 45, 0xD6990624)
ss[5], ss[1] = RND(ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 46, 0xF40E3585)
ss[4], ss[0] = RND(ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 47, 0x106AA070)
ss[3], ss[7] = RND(ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 48, 0x19A4C116)
ss[2], ss[6] = RND(ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 49, 0x1E376C08)
ss[1], ss[5] = RND(ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 50, 0x2748774C)
ss[0], ss[4] = RND(ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 51, 0x34B0BCB5)
ss[7], ss[3] = RND(ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 52, 0x391C0CB3)
ss[6], ss[2] = RND(ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 53, 0x4ED8AA4A)
ss[5], ss[1] = RND(ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 54, 0x5B9CCA4F)
ss[4], ss[0] = RND(ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 55, 0x682E6FF3)
ss[3], ss[7] = RND(ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 56, 0x748F82EE)
ss[2], ss[6] = RND(ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 57, 0x78A5636F)
ss[1], ss[5] = RND(ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 58, 0x84C87814)
ss[0], ss[4] = RND(ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 59, 0x8CC70208)
ss[7], ss[3] = RND(ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 60, 0x90BEFFFA)
ss[6], ss[2] = RND(ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 61, 0xA4506CEB)
ss[5], ss[1] = RND(ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 62, 0xBEF9A3F7)
ss[4], ss[0] = RND(ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 63, 0xC67178F2)
dig = []
for i, x in enumerate(sha_info["digest"]):
dig.append((x + ss[i]) & 0xFFFFFFFF)
sha_info["digest"] = dig
def sha_init():
sha_info = new_shaobject()
sha_info["digest"] = [
0x6A09E667,
0xBB67AE85,
0x3C6EF372,
0xA54FF53A,
0x510E527F,
0x9B05688C,
0x1F83D9AB,
0x5BE0CD19,
]
sha_info["count_lo"] = 0
sha_info["count_hi"] = 0
sha_info["local"] = 0
sha_info["digestsize"] = 32
return sha_info
def sha224_init():
sha_info = new_shaobject()
sha_info["digest"] = [
0xC1059ED8,
0x367CD507,
0x3070DD17,
0xF70E5939,
0xFFC00B31,
0x68581511,
0x64F98FA7,
0xBEFA4FA4,
]
sha_info["count_lo"] = 0
sha_info["count_hi"] = 0
sha_info["local"] = 0
sha_info["digestsize"] = 28
return sha_info
def getbuf(s):
if isinstance(s, str):
return s.encode("ascii")
else:
return bytes(s)
def sha_update(sha_info, buffer):
if isinstance(buffer, str):
raise TypeError("Unicode strings must be encoded before hashing")
count = len(buffer)
buffer_idx = 0
clo = (sha_info["count_lo"] + (count << 3)) & 0xFFFFFFFF
if clo < sha_info["count_lo"]:
sha_info["count_hi"] += 1
sha_info["count_lo"] = clo
sha_info["count_hi"] += count >> 29
if sha_info["local"]:
i = SHA_BLOCKSIZE - sha_info["local"]
if i > count:
i = count
# copy buffer
for x in enumerate(buffer[buffer_idx : buffer_idx + i]):
sha_info["data"][sha_info["local"] + x[0]] = x[1]
count -= i
buffer_idx += i
sha_info["local"] += i
if sha_info["local"] == SHA_BLOCKSIZE:
sha_transform(sha_info)
sha_info["local"] = 0
else:
return
while count >= SHA_BLOCKSIZE:
# copy buffer
sha_info["data"] = list(buffer[buffer_idx : buffer_idx + SHA_BLOCKSIZE])
count -= SHA_BLOCKSIZE
buffer_idx += SHA_BLOCKSIZE
sha_transform(sha_info)
# copy buffer
pos = sha_info["local"]
sha_info["data"][pos : pos + count] = list(buffer[buffer_idx : buffer_idx + count])
sha_info["local"] = count
def sha_final(sha_info):
lo_bit_count = sha_info["count_lo"]
hi_bit_count = sha_info["count_hi"]
count = (lo_bit_count >> 3) & 0x3F
sha_info["data"][count] = 0x80
count += 1
if count > SHA_BLOCKSIZE - 8:
# zero the bytes in data after the count
sha_info["data"] = sha_info["data"][:count] + ([0] * (SHA_BLOCKSIZE - count))
sha_transform(sha_info)
# zero bytes in data
sha_info["data"] = [0] * SHA_BLOCKSIZE
else:
sha_info["data"] = sha_info["data"][:count] + ([0] * (SHA_BLOCKSIZE - count))
sha_info["data"][56] = (hi_bit_count >> 24) & 0xFF
sha_info["data"][57] = (hi_bit_count >> 16) & 0xFF
sha_info["data"][58] = (hi_bit_count >> 8) & 0xFF
sha_info["data"][59] = (hi_bit_count >> 0) & 0xFF
sha_info["data"][60] = (lo_bit_count >> 24) & 0xFF
sha_info["data"][61] = (lo_bit_count >> 16) & 0xFF
sha_info["data"][62] = (lo_bit_count >> 8) & 0xFF
sha_info["data"][63] = (lo_bit_count >> 0) & 0xFF
sha_transform(sha_info)
dig = []
for i in sha_info["digest"]:
dig.extend([((i >> 24) & 0xFF), ((i >> 16) & 0xFF), ((i >> 8) & 0xFF), (i & 0xFF)])
return bytes(dig)
class sha256(object):
digest_size = digestsize = SHA_DIGESTSIZE
block_size = SHA_BLOCKSIZE
def __init__(self, s=None):
self._sha = sha_init()
if s:
sha_update(self._sha, getbuf(s))
def update(self, s):
sha_update(self._sha, getbuf(s))
def digest(self):
return sha_final(self._sha.copy())[: self._sha["digestsize"]]
def hexdigest(self):
return "".join(["%.2x" % i for i in self.digest()])
def copy(self):
new = sha256()
new._sha = self._sha.copy()
return new
class sha224(sha256):
digest_size = digestsize = 28
def __init__(self, s=None):
self._sha = sha224_init()
if s:
sha_update(self._sha, getbuf(s))
def copy(self):
new = sha224()
new._sha = self._sha.copy()
return new
def test():
a_str = "just a test string"
assert (
b"\xe3\xb0\xc4B\x98\xfc\x1c\x14\x9a\xfb\xf4\xc8\x99o\xb9$'\xaeA\xe4d\x9b\x93L\xa4\x95\x99\x1bxR\xb8U"
== sha256().digest()
)
assert (
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" == sha256().hexdigest()
)
assert (
"d7b553c6f09ac85d142415f857c5310f3bbbe7cdd787cce4b985acedd585266f"
== sha256(a_str).hexdigest()
)
assert (
"8113ebf33c97daa9998762aacafe750c7cefc2b2f173c90c59663a57fe626f21"
== sha256(a_str * 7).hexdigest()
)
s = sha256(a_str)
s.update(a_str)
assert "03d9963e05a094593190b6fc794cb1a3e1ac7d7883f0b5855268afeccc70d461" == s.hexdigest()
if __name__ == "__main__":
test()

View File

@@ -1 +0,0 @@
from ._sha512 import sha384

View File

@@ -1,519 +0,0 @@
"""
This code was Ported from CPython's sha512module.c
"""
SHA_BLOCKSIZE = 128
SHA_DIGESTSIZE = 64
def new_shaobject():
return {
"digest": [0] * 8,
"count_lo": 0,
"count_hi": 0,
"data": [0] * SHA_BLOCKSIZE,
"local": 0,
"digestsize": 0,
}
ROR64 = (
lambda x, y: (((x & 0xFFFFFFFFFFFFFFFF) >> (y & 63)) | (x << (64 - (y & 63))))
& 0xFFFFFFFFFFFFFFFF
)
Ch = lambda x, y, z: (z ^ (x & (y ^ z)))
Maj = lambda x, y, z: (((x | y) & z) | (x & y))
S = lambda x, n: ROR64(x, n)
R = lambda x, n: (x & 0xFFFFFFFFFFFFFFFF) >> n
Sigma0 = lambda x: (S(x, 28) ^ S(x, 34) ^ S(x, 39))
Sigma1 = lambda x: (S(x, 14) ^ S(x, 18) ^ S(x, 41))
Gamma0 = lambda x: (S(x, 1) ^ S(x, 8) ^ R(x, 7))
Gamma1 = lambda x: (S(x, 19) ^ S(x, 61) ^ R(x, 6))
def sha_transform(sha_info):
W = []
d = sha_info["data"]
for i in range(0, 16):
W.append(
(d[8 * i] << 56)
+ (d[8 * i + 1] << 48)
+ (d[8 * i + 2] << 40)
+ (d[8 * i + 3] << 32)
+ (d[8 * i + 4] << 24)
+ (d[8 * i + 5] << 16)
+ (d[8 * i + 6] << 8)
+ d[8 * i + 7]
)
for i in range(16, 80):
W.append(
(Gamma1(W[i - 2]) + W[i - 7] + Gamma0(W[i - 15]) + W[i - 16]) & 0xFFFFFFFFFFFFFFFF
)
ss = sha_info["digest"][:]
def RND(a, b, c, d, e, f, g, h, i, ki):
t0 = (h + Sigma1(e) + Ch(e, f, g) + ki + W[i]) & 0xFFFFFFFFFFFFFFFF
t1 = (Sigma0(a) + Maj(a, b, c)) & 0xFFFFFFFFFFFFFFFF
d = (d + t0) & 0xFFFFFFFFFFFFFFFF
h = (t0 + t1) & 0xFFFFFFFFFFFFFFFF
return d & 0xFFFFFFFFFFFFFFFF, h & 0xFFFFFFFFFFFFFFFF
ss[3], ss[7] = RND(
ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 0, 0x428A2F98D728AE22
)
ss[2], ss[6] = RND(
ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 1, 0x7137449123EF65CD
)
ss[1], ss[5] = RND(
ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 2, 0xB5C0FBCFEC4D3B2F
)
ss[0], ss[4] = RND(
ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 3, 0xE9B5DBA58189DBBC
)
ss[7], ss[3] = RND(
ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 4, 0x3956C25BF348B538
)
ss[6], ss[2] = RND(
ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 5, 0x59F111F1B605D019
)
ss[5], ss[1] = RND(
ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 6, 0x923F82A4AF194F9B
)
ss[4], ss[0] = RND(
ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 7, 0xAB1C5ED5DA6D8118
)
ss[3], ss[7] = RND(
ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 8, 0xD807AA98A3030242
)
ss[2], ss[6] = RND(
ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 9, 0x12835B0145706FBE
)
ss[1], ss[5] = RND(
ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 10, 0x243185BE4EE4B28C
)
ss[0], ss[4] = RND(
ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 11, 0x550C7DC3D5FFB4E2
)
ss[7], ss[3] = RND(
ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 12, 0x72BE5D74F27B896F
)
ss[6], ss[2] = RND(
ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 13, 0x80DEB1FE3B1696B1
)
ss[5], ss[1] = RND(
ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 14, 0x9BDC06A725C71235
)
ss[4], ss[0] = RND(
ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 15, 0xC19BF174CF692694
)
ss[3], ss[7] = RND(
ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 16, 0xE49B69C19EF14AD2
)
ss[2], ss[6] = RND(
ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 17, 0xEFBE4786384F25E3
)
ss[1], ss[5] = RND(
ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 18, 0x0FC19DC68B8CD5B5
)
ss[0], ss[4] = RND(
ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 19, 0x240CA1CC77AC9C65
)
ss[7], ss[3] = RND(
ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 20, 0x2DE92C6F592B0275
)
ss[6], ss[2] = RND(
ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 21, 0x4A7484AA6EA6E483
)
ss[5], ss[1] = RND(
ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 22, 0x5CB0A9DCBD41FBD4
)
ss[4], ss[0] = RND(
ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 23, 0x76F988DA831153B5
)
ss[3], ss[7] = RND(
ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 24, 0x983E5152EE66DFAB
)
ss[2], ss[6] = RND(
ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 25, 0xA831C66D2DB43210
)
ss[1], ss[5] = RND(
ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 26, 0xB00327C898FB213F
)
ss[0], ss[4] = RND(
ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 27, 0xBF597FC7BEEF0EE4
)
ss[7], ss[3] = RND(
ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 28, 0xC6E00BF33DA88FC2
)
ss[6], ss[2] = RND(
ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 29, 0xD5A79147930AA725
)
ss[5], ss[1] = RND(
ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 30, 0x06CA6351E003826F
)
ss[4], ss[0] = RND(
ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 31, 0x142929670A0E6E70
)
ss[3], ss[7] = RND(
ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 32, 0x27B70A8546D22FFC
)
ss[2], ss[6] = RND(
ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 33, 0x2E1B21385C26C926
)
ss[1], ss[5] = RND(
ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 34, 0x4D2C6DFC5AC42AED
)
ss[0], ss[4] = RND(
ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 35, 0x53380D139D95B3DF
)
ss[7], ss[3] = RND(
ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 36, 0x650A73548BAF63DE
)
ss[6], ss[2] = RND(
ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 37, 0x766A0ABB3C77B2A8
)
ss[5], ss[1] = RND(
ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 38, 0x81C2C92E47EDAEE6
)
ss[4], ss[0] = RND(
ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 39, 0x92722C851482353B
)
ss[3], ss[7] = RND(
ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 40, 0xA2BFE8A14CF10364
)
ss[2], ss[6] = RND(
ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 41, 0xA81A664BBC423001
)
ss[1], ss[5] = RND(
ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 42, 0xC24B8B70D0F89791
)
ss[0], ss[4] = RND(
ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 43, 0xC76C51A30654BE30
)
ss[7], ss[3] = RND(
ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 44, 0xD192E819D6EF5218
)
ss[6], ss[2] = RND(
ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 45, 0xD69906245565A910
)
ss[5], ss[1] = RND(
ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 46, 0xF40E35855771202A
)
ss[4], ss[0] = RND(
ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 47, 0x106AA07032BBD1B8
)
ss[3], ss[7] = RND(
ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 48, 0x19A4C116B8D2D0C8
)
ss[2], ss[6] = RND(
ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 49, 0x1E376C085141AB53
)
ss[1], ss[5] = RND(
ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 50, 0x2748774CDF8EEB99
)
ss[0], ss[4] = RND(
ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 51, 0x34B0BCB5E19B48A8
)
ss[7], ss[3] = RND(
ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 52, 0x391C0CB3C5C95A63
)
ss[6], ss[2] = RND(
ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 53, 0x4ED8AA4AE3418ACB
)
ss[5], ss[1] = RND(
ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 54, 0x5B9CCA4F7763E373
)
ss[4], ss[0] = RND(
ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 55, 0x682E6FF3D6B2B8A3
)
ss[3], ss[7] = RND(
ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 56, 0x748F82EE5DEFB2FC
)
ss[2], ss[6] = RND(
ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 57, 0x78A5636F43172F60
)
ss[1], ss[5] = RND(
ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 58, 0x84C87814A1F0AB72
)
ss[0], ss[4] = RND(
ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 59, 0x8CC702081A6439EC
)
ss[7], ss[3] = RND(
ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 60, 0x90BEFFFA23631E28
)
ss[6], ss[2] = RND(
ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 61, 0xA4506CEBDE82BDE9
)
ss[5], ss[1] = RND(
ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 62, 0xBEF9A3F7B2C67915
)
ss[4], ss[0] = RND(
ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 63, 0xC67178F2E372532B
)
ss[3], ss[7] = RND(
ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 64, 0xCA273ECEEA26619C
)
ss[2], ss[6] = RND(
ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 65, 0xD186B8C721C0C207
)
ss[1], ss[5] = RND(
ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 66, 0xEADA7DD6CDE0EB1E
)
ss[0], ss[4] = RND(
ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 67, 0xF57D4F7FEE6ED178
)
ss[7], ss[3] = RND(
ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 68, 0x06F067AA72176FBA
)
ss[6], ss[2] = RND(
ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 69, 0x0A637DC5A2C898A6
)
ss[5], ss[1] = RND(
ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 70, 0x113F9804BEF90DAE
)
ss[4], ss[0] = RND(
ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 71, 0x1B710B35131C471B
)
ss[3], ss[7] = RND(
ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], 72, 0x28DB77F523047D84
)
ss[2], ss[6] = RND(
ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], 73, 0x32CAAB7B40C72493
)
ss[1], ss[5] = RND(
ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], ss[5], 74, 0x3C9EBE0A15C9BEBC
)
ss[0], ss[4] = RND(
ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], ss[4], 75, 0x431D67C49C100D4C
)
ss[7], ss[3] = RND(
ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], ss[3], 76, 0x4CC5D4BECB3E42B6
)
ss[6], ss[2] = RND(
ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], ss[2], 77, 0x597F299CFC657E2A
)
ss[5], ss[1] = RND(
ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], ss[1], 78, 0x5FCB6FAB3AD6FAEC
)
ss[4], ss[0] = RND(
ss[1], ss[2], ss[3], ss[4], ss[5], ss[6], ss[7], ss[0], 79, 0x6C44198C4A475817
)
dig = []
for i, x in enumerate(sha_info["digest"]):
dig.append((x + ss[i]) & 0xFFFFFFFFFFFFFFFF)
sha_info["digest"] = dig
def sha_init():
sha_info = new_shaobject()
sha_info["digest"] = [
0x6A09E667F3BCC908,
0xBB67AE8584CAA73B,
0x3C6EF372FE94F82B,
0xA54FF53A5F1D36F1,
0x510E527FADE682D1,
0x9B05688C2B3E6C1F,
0x1F83D9ABFB41BD6B,
0x5BE0CD19137E2179,
]
sha_info["count_lo"] = 0
sha_info["count_hi"] = 0
sha_info["local"] = 0
sha_info["digestsize"] = 64
return sha_info
def sha384_init():
sha_info = new_shaobject()
sha_info["digest"] = [
0xCBBB9D5DC1059ED8,
0x629A292A367CD507,
0x9159015A3070DD17,
0x152FECD8F70E5939,
0x67332667FFC00B31,
0x8EB44A8768581511,
0xDB0C2E0D64F98FA7,
0x47B5481DBEFA4FA4,
]
sha_info["count_lo"] = 0
sha_info["count_hi"] = 0
sha_info["local"] = 0
sha_info["digestsize"] = 48
return sha_info
def getbuf(s):
if isinstance(s, str):
return s.encode("ascii")
else:
return bytes(s)
def sha_update(sha_info, buffer):
if isinstance(buffer, str):
raise TypeError("Unicode strings must be encoded before hashing")
count = len(buffer)
buffer_idx = 0
clo = (sha_info["count_lo"] + (count << 3)) & 0xFFFFFFFF
if clo < sha_info["count_lo"]:
sha_info["count_hi"] += 1
sha_info["count_lo"] = clo
sha_info["count_hi"] += count >> 29
if sha_info["local"]:
i = SHA_BLOCKSIZE - sha_info["local"]
if i > count:
i = count
# copy buffer
for x in enumerate(buffer[buffer_idx : buffer_idx + i]):
sha_info["data"][sha_info["local"] + x[0]] = x[1]
count -= i
buffer_idx += i
sha_info["local"] += i
if sha_info["local"] == SHA_BLOCKSIZE:
sha_transform(sha_info)
sha_info["local"] = 0
else:
return
while count >= SHA_BLOCKSIZE:
# copy buffer
sha_info["data"] = list(buffer[buffer_idx : buffer_idx + SHA_BLOCKSIZE])
count -= SHA_BLOCKSIZE
buffer_idx += SHA_BLOCKSIZE
sha_transform(sha_info)
# copy buffer
pos = sha_info["local"]
sha_info["data"][pos : pos + count] = list(buffer[buffer_idx : buffer_idx + count])
sha_info["local"] = count
def sha_final(sha_info):
lo_bit_count = sha_info["count_lo"]
hi_bit_count = sha_info["count_hi"]
count = (lo_bit_count >> 3) & 0x7F
sha_info["data"][count] = 0x80
count += 1
if count > SHA_BLOCKSIZE - 16:
# zero the bytes in data after the count
sha_info["data"] = sha_info["data"][:count] + ([0] * (SHA_BLOCKSIZE - count))
sha_transform(sha_info)
# zero bytes in data
sha_info["data"] = [0] * SHA_BLOCKSIZE
else:
sha_info["data"] = sha_info["data"][:count] + ([0] * (SHA_BLOCKSIZE - count))
sha_info["data"][112] = 0
sha_info["data"][113] = 0
sha_info["data"][114] = 0
sha_info["data"][115] = 0
sha_info["data"][116] = 0
sha_info["data"][117] = 0
sha_info["data"][118] = 0
sha_info["data"][119] = 0
sha_info["data"][120] = (hi_bit_count >> 24) & 0xFF
sha_info["data"][121] = (hi_bit_count >> 16) & 0xFF
sha_info["data"][122] = (hi_bit_count >> 8) & 0xFF
sha_info["data"][123] = (hi_bit_count >> 0) & 0xFF
sha_info["data"][124] = (lo_bit_count >> 24) & 0xFF
sha_info["data"][125] = (lo_bit_count >> 16) & 0xFF
sha_info["data"][126] = (lo_bit_count >> 8) & 0xFF
sha_info["data"][127] = (lo_bit_count >> 0) & 0xFF
sha_transform(sha_info)
dig = []
for i in sha_info["digest"]:
dig.extend(
[
((i >> 56) & 0xFF),
((i >> 48) & 0xFF),
((i >> 40) & 0xFF),
((i >> 32) & 0xFF),
((i >> 24) & 0xFF),
((i >> 16) & 0xFF),
((i >> 8) & 0xFF),
(i & 0xFF),
]
)
return bytes(dig)
class sha512(object):
digest_size = digestsize = SHA_DIGESTSIZE
block_size = SHA_BLOCKSIZE
def __init__(self, s=None):
self._sha = sha_init()
if s:
sha_update(self._sha, getbuf(s))
def update(self, s):
sha_update(self._sha, getbuf(s))
def digest(self):
return sha_final(self._sha.copy())[: self._sha["digestsize"]]
def hexdigest(self):
return "".join(["%.2x" % i for i in self.digest()])
def copy(self):
new = sha512()
new._sha = self._sha.copy()
return new
class sha384(sha512):
digest_size = digestsize = 48
def __init__(self, s=None):
self._sha = sha384_init()
if s:
sha_update(self._sha, getbuf(s))
def copy(self):
new = sha384()
new._sha = self._sha.copy()
return new
def test():
a_str = "just a test string"
assert (
sha512().digest()
== b"\xcf\x83\xe15~\xef\xb8\xbd\xf1T(P\xd6m\x80\x07\xd6 \xe4\x05\x0bW\x15\xdc\x83\xf4\xa9!\xd3l\xe9\xceG\xd0\xd1<]\x85\xf2\xb0\xff\x83\x18\xd2\x87~\xec/c\xb91\xbdGAz\x81\xa582z\xf9'\xda>"
)
assert (
sha512().hexdigest()
== "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"
)
assert (
sha512(a_str).hexdigest()
== "68be4c6664af867dd1d01c8d77e963d87d77b702400c8fabae355a41b8927a5a5533a7f1c28509bbd65c5f3ac716f33be271fbda0ca018b71a84708c9fae8a53"
)
assert (
sha512(a_str * 7).hexdigest()
== "3233acdbfcfff9bff9fc72401d31dbffa62bd24e9ec846f0578d647da73258d9f0879f7fde01fe2cc6516af3f343807fdef79e23d696c923d79931db46bf1819"
)
s = sha512(a_str)
s.update(a_str)
assert (
s.hexdigest()
== "341aeb668730bbb48127d5531115f3c39d12cb9586a6ca770898398aff2411087cfe0b570689adf328cddeb1f00803acce6737a19f310b53bbdb0320828f75bb"
)
if __name__ == "__main__":
test()

View File

@@ -1,152 +1,87 @@
"""HMAC (Keyed-Hashing for Message Authentication) Python module.
Implements the HMAC algorithm as described by RFC 2104.
"""
import warnings as _warnings
# from _operator import _compare_digest as compare_digest
import hashlib as _hashlib
PendingDeprecationWarning = None
RuntimeWarning = None
trans_5C = bytes((x ^ 0x5C) for x in range(256))
trans_36 = bytes((x ^ 0x36) for x in range(256))
def translate(d, t):
return bytes(t[x] for x in d)
# The size of the digests returned by HMAC depends on the underlying
# hashing module used. Use digest_size from the instance of HMAC instead.
digest_size = None
# Implements the hmac module from the Python standard library.
class HMAC:
"""RFC 2104 HMAC class. Also complies with RFC 4231.
This supports the API for Cryptographic Hash Functions (PEP 247).
"""
blocksize = 64 # 512-bit HMAC; can be changed in subclasses.
def __init__(self, key, msg=None, digestmod=None):
"""Create a new HMAC object.
key: key for the keyed hash object.
msg: Initial input for the hash, if provided.
digestmod: A module supporting PEP 247. *OR*
A hashlib constructor returning a new hash object. *OR*
A hash name suitable for hashlib.new().
Defaults to hashlib.md5.
Implicit default to hashlib.md5 is deprecated and will be
removed in Python 3.6.
Note: key and msg must be a bytes or bytearray objects.
"""
if not isinstance(key, (bytes, bytearray)):
raise TypeError("key: expected bytes or bytearray, but got %r" % type(key).__name__)
raise TypeError("key: expected bytes/bytearray")
import hashlib
if digestmod is None:
_warnings.warn(
"HMAC() without an explicit digestmod argument " "is deprecated.",
PendingDeprecationWarning,
2,
)
digestmod = _hashlib.md5
# TODO: Default hash algorithm is now deprecated.
digestmod = hashlib.md5
if callable(digestmod):
self.digest_cons = digestmod
# A hashlib constructor returning a new hash object.
make_hash = digestmod # A
elif isinstance(digestmod, str):
self.digest_cons = lambda d=b"": _hashlib.new(digestmod, d)
# A hash name suitable for hashlib.new().
make_hash = lambda d=b"": hashlib.new(digestmod, d) # B
else:
self.digest_cons = lambda d=b"": digestmod.new(d)
# A module supporting PEP 247.
make_hash = digestmod.new # C
self.outer = self.digest_cons()
self.inner = self.digest_cons()
self.digest_size = self.inner.digest_size
self._outer = make_hash()
self._inner = make_hash()
if hasattr(self.inner, "block_size"):
blocksize = self.inner.block_size
if blocksize < 16:
_warnings.warn(
"block_size of %d seems too small; using our "
"default of %d." % (blocksize, self.blocksize),
RuntimeWarning,
2,
)
blocksize = self.blocksize
else:
_warnings.warn(
"No block_size attribute on given digest object; "
"Assuming %d." % (self.blocksize),
RuntimeWarning,
2,
)
blocksize = self.blocksize
self.digest_size = getattr(self._inner, "digest_size", None)
# If the provided hash doesn't support block_size (e.g. built-in
# hashlib), 64 is the correct default for all built-in hash
# functions (md5, sha1, sha256).
self.block_size = getattr(self._inner, "block_size", 64)
# self.blocksize is the default blocksize. self.block_size is
# effective block size as well as the public API attribute.
self.block_size = blocksize
# Truncate to digest_size if greater than block_size.
if len(key) > self.block_size:
key = make_hash(key).digest()
if len(key) > blocksize:
key = self.digest_cons(key).digest()
# Pad to block size.
key = key + bytes(self.block_size - len(key))
self._outer.update(bytes(x ^ 0x5C for x in key))
self._inner.update(bytes(x ^ 0x36 for x in key))
key = key + bytes(blocksize - len(key))
self.outer.update(translate(key, trans_5C))
self.inner.update(translate(key, trans_36))
if msg is not None:
self.update(msg)
@property
def name(self):
return "hmac-" + self.inner.name
return "hmac-" + getattr(self._inner, "name", type(self._inner).__name__)
def update(self, msg):
"""Update this hashing object with the string msg."""
self.inner.update(msg)
self._inner.update(msg)
def copy(self):
"""Return a separate copy of this hashing object.
An update to this copy won't affect the original object.
"""
if not hasattr(self._inner, "copy"):
# Not supported for built-in hash functions.
raise NotImplementedError()
# Call __new__ directly to avoid the expensive __init__.
other = self.__class__.__new__(self.__class__)
other.digest_cons = self.digest_cons
other.block_size = self.block_size
other.digest_size = self.digest_size
other.inner = self.inner.copy()
other.outer = self.outer.copy()
other._inner = self._inner.copy()
other._outer = self._outer.copy()
return other
def _current(self):
"""Return a hash object for the current state.
To be used only internally with digest() and hexdigest().
"""
h = self.outer.copy()
h.update(self.inner.digest())
h = self._outer
if hasattr(h, "copy"):
# built-in hash functions don't support this, and as a result,
# digest() will finalise the hmac and further calls to
# update/digest will fail.
h = h.copy()
h.update(self._inner.digest())
return h
def digest(self):
"""Return the hash value of this hashing object.
This returns a string containing 8-bit data. The object is
not altered in any way by this function; you can continue
updating the object after calling this function.
"""
h = self._current()
return h.digest()
def hexdigest(self):
"""Like digest(), but returns a string of hexadecimal digits instead."""
h = self._current()
return h.hexdigest()
import binascii
return str(binascii.hexlify(self.digest()), "utf-8")
def new(key, msg=None, digestmod=None):
"""Create a new hashing object and return it.
key: The starting key for the hash.
msg: if available, will immediately be hashed into the object's starting
state.
You can now feed arbitrary strings into the object using its update()
method, and can ask for the hash value at any time by calling its digest()
method.
"""
return HMAC(key, msg, digestmod)

View File

@@ -4,7 +4,6 @@ import hmac
import json
from time import time
def _to_b64url(data):
return (
binascii.b2a_base64(data)
@@ -32,13 +31,13 @@ class exceptions:
class InvalidSignatureError(PyJWTError):
pass
class ExpiredTokenError(PyJWTError):
class ExpiredSignatureError(PyJWTError):
pass
def encode(payload, key, algorithm="HS256"):
if algorithm != "HS256":
raise exceptions.InvalidAlgorithmError()
raise exceptions.InvalidAlgorithmError
if isinstance(key, str):
key = key.encode()
@@ -50,30 +49,30 @@ def encode(payload, key, algorithm="HS256"):
def decode(token, key, algorithms=["HS256"]):
if "HS256" not in algorithms:
raise exceptions.InvalidAlgorithmError()
raise exceptions.InvalidAlgorithmError
parts = token.encode().split(b".")
if len(parts) != 3:
raise exceptions.InvalidTokenError()
raise exceptions.InvalidTokenError
try:
header = json.loads(_from_b64url(parts[0]).decode())
payload = json.loads(_from_b64url(parts[1]).decode())
signature = _from_b64url(parts[2])
except Exception:
raise exceptions.InvalidTokenError()
raise exceptions.InvalidTokenError
if header["alg"] not in algorithms or header["alg"] != "HS256":
raise exceptions.InvalidAlgorithmError()
raise exceptions.InvalidAlgorithmError
if isinstance(key, str):
key = key.encode()
calculated_signature = hmac.new(key, parts[0] + b"." + parts[1], hashlib.sha256).digest()
if signature != calculated_signature:
raise exceptions.InvalidSignatureError()
raise exceptions.InvalidSignatureError
if "exp" in payload:
if time() > payload["exp"]:
raise exceptions.ExpiredTokenError()
raise exceptions.ExpiredSignatureError
return payload

View File

@@ -1,2 +0,0 @@
def warn(msg, cat=None, stacklevel=1):
print("%s: %s" % ("Warning" if cat is None else cat.__name__, msg))

View File

@@ -1,6 +1,6 @@
[metadata]
name = microdot
version = 1.0.0
version = 1.1.0
author = Miguel Grinberg
author_email = miguel.grinberg@gmail.com
description = The impossibly small web framework for MicroPython
@@ -28,7 +28,11 @@ py_modules =
microdot_utemplate
microdot_jinja
microdot_session
microdot_websocket
microdot_websocket_alt
microdot_asyncio_websocket
microdot_test_client
microdot_asyncio_test_client
microdot_wsgi
microdot_asgi
microdot_asgi_websocket

View File

@@ -51,12 +51,19 @@ except ImportError:
except ImportError: # pragma: no cover
socket = None
MUTED_SOCKET_ERRORS = [
32, # Broken pipe
54, # Connection reset by peer
104, # Connection reset by peer
128, # Operation on closed socket
]
def urldecode(string):
string = string.replace('+', ' ')
parts = string.split('%')
def urldecode_str(s):
s = s.replace('+', ' ')
parts = s.split('%')
if len(parts) == 1:
return string
return s
result = [parts[0]]
for item in parts[1:]:
if item == '':
@@ -68,6 +75,22 @@ def urldecode(string):
return ''.join(result)
def urldecode_bytes(s):
s = s.replace(b'+', b' ')
parts = s.split(b'%')
if len(parts) == 1:
return s.decode()
result = [parts[0]]
for item in parts[1:]:
if item == b'':
result.append(b'%')
else:
code = item[:2]
result.append(bytes([int(code, 16)]))
result.append(item[2:])
return b''.join(result).decode()
class MultiDict(dict):
"""A subclass of dictionary that can hold multiple values for the same
key. It is used to hold key/value pairs decoded from query strings and
@@ -194,7 +217,7 @@ class Request():
pass
def __init__(self, app, client_addr, method, url, http_version, headers,
body=None, stream=None):
body=None, stream=None, sock=None):
#: The application instance to which this request belongs.
self.app = app
#: The address of the client, as a tuple (host, port).
@@ -240,18 +263,21 @@ class Request():
self.body_used = False
self._stream = stream
self.stream_used = False
self.sock = sock
self._json = None
self._form = None
self.after_request_handlers = []
@staticmethod
def create(app, client_stream, client_addr):
def create(app, client_stream, client_addr, client_sock=None):
"""Create a request object.
:param app: The Microdot application instance.
:param client_stream: An input stream from where the request data can
be read.
:param client_addr: The address of the client, as a tuple.
:param client_sock: The low-level socket associated with the request.
This method returns a newly created ``Request`` object.
"""
@@ -273,13 +299,19 @@ class Request():
headers[header] = value
return Request(app, client_addr, method, url, http_version, headers,
stream=client_stream)
stream=client_stream, sock=client_sock)
def _parse_urlencoded(self, urlencoded):
data = MultiDict()
if urlencoded:
for k, v in [pair.split('=', 1) for pair in urlencoded.split('&')]:
data[urldecode(k)] = urldecode(v)
if len(urlencoded) > 0:
if isinstance(urlencoded, str):
for k, v in [pair.split('=', 1)
for pair in urlencoded.split('&')]:
data[urldecode_str(k)] = urldecode_str(v)
elif isinstance(urlencoded, bytes): # pragma: no branch
for k, v in [pair.split(b'=', 1)
for pair in urlencoded.split(b'&')]:
data[urldecode_bytes(k)] = urldecode_bytes(v)
return data
@property
@@ -332,7 +364,7 @@ class Request():
mime_type = self.content_type.split(';')[0]
if mime_type != 'application/x-www-form-urlencoded':
return None
self._form = self._parse_urlencoded(self.body.decode())
self._form = self._parse_urlencoded(self.body)
return self._form
def after_request(self, f):
@@ -396,6 +428,10 @@ class Response():
#: ``Content-Type`` header.
default_content_type = 'text/plain'
#: Special response used to signal that a response does not need to be
#: written to the client. Used to exit WebSocket connections cleanly.
already_handled = None
def __init__(self, body='', status_code=200, headers=None, reason=None):
if body is None and status_code == 200:
body = ''
@@ -405,7 +441,7 @@ class Response():
self.reason = reason
if isinstance(body, (dict, list)):
self.body = json.dumps(body).encode()
self.headers['Content-Type'] = 'application/json'
self.headers['Content-Type'] = 'application/json; charset=UTF-8'
elif isinstance(body, str):
self.body = body.encode()
else:
@@ -454,6 +490,8 @@ class Response():
self.headers['Content-Length'] = str(len(self.body))
if 'Content-Type' not in self.headers:
self.headers['Content-Type'] = self.default_content_type
if 'charset=' not in self.headers['Content-Type']:
self.headers['Content-Type'] += '; charset=UTF-8'
def write(self, stream):
self.complete()
@@ -482,7 +520,7 @@ class Response():
if can_flush: # pragma: no cover
stream.flush()
except OSError as exc: # pragma: no cover
if exc.errno == 32: # errno.EPIPE
if exc.errno in MUTED_SOCKET_ERRORS:
pass
else:
raise
@@ -594,6 +632,15 @@ class URLPattern():
return args
class HTTPException(Exception):
def __init__(self, status_code, reason=None):
self.status_code = status_code
self.reason = reason or str(status_code) + ' error'
def __repr__(self): # pragma: no cover
return 'HTTPException: {}'.format(self.status_code)
class Microdot():
"""An HTTP application class.
@@ -816,7 +863,29 @@ class Microdot():
for status_code, handler in subapp.error_handlers.items():
self.error_handlers[status_code] = handler
def run(self, host='0.0.0.0', port=5000, debug=False):
@staticmethod
def abort(status_code, reason=None):
"""Abort the current request and return an error response with the
given status code.
:param status_code: The numeric status code of the response.
:param reason: The reason for the response, which is included in the
response body.
Example::
from microdot import abort
@app.route('/users/<int:id>')
def get_user(id):
user = get_user_by_id(id)
if user is None:
abort(404)
return user.to_dict()
"""
raise HTTPException(status_code, reason)
def run(self, host='0.0.0.0', port=5000, debug=False, ssl=None):
"""Start the web server. This function does not normally return, as
the server enters an endless listening loop. The :func:`shutdown`
function provides a method for terminating the server gracefully.
@@ -832,6 +901,8 @@ class Microdot():
port 5000.
:param debug: If ``True``, the server logs debugging information. The
default is ``False``.
:param ssl: An ``SSLContext`` instance or ``None`` if the server should
not use TLS. The default is ``None``.
Example::
@@ -859,6 +930,9 @@ class Microdot():
self.server.bind(addr)
self.server.listen(5)
if ssl:
self.server = ssl.wrap_socket(self.server, server_side=True)
while not self.shutdown_requested:
try:
sock, addr = self.server.accept()
@@ -866,8 +940,11 @@ class Microdot():
if exc.errno == errno.ECONNABORTED:
break
else:
raise
create_thread(self.handle_request, sock, addr)
print_exception(exc)
except Exception as exc: # pragma: no cover
print_exception(exc)
else:
create_thread(self.handle_request, sock, addr)
def shutdown(self):
"""Request a server shutdown. The server will then exit its request
@@ -903,19 +980,23 @@ class Microdot():
stream = sock
req = None
res = None
try:
req = Request.create(self, stream, addr)
req = Request.create(self, stream, addr, sock)
res = self.dispatch_request(req)
except Exception as exc: # pragma: no cover
print_exception(exc)
res = self.dispatch_request(req)
res.write(stream)
try:
if res and res != Response.already_handled: # pragma: no branch
res.write(stream)
stream.close()
except OSError as exc: # pragma: no cover
if exc.errno == 32: # errno.EPIPE
if exc.errno in MUTED_SOCKET_ERRORS:
pass
else:
raise
print_exception(exc)
except Exception as exc: # pragma: no cover
print_exception(exc)
if stream != sock: # pragma: no cover
sock.close()
if self.shutdown_requested: # pragma: no cover
@@ -962,6 +1043,12 @@ class Microdot():
res = self.error_handlers[f](req)
else:
res = 'Not found', f
except HTTPException as exc:
print_exception(exc)
if exc.status_code in self.error_handlers:
res = self.error_handlers[exc.status_code](req)
else:
res = exc.reason, exc.status_code
except Exception as exc:
print_exception(exc)
res = None
@@ -988,5 +1075,7 @@ class Microdot():
return res
abort = Microdot.abort
Response.already_handled = Response()
redirect = Response.redirect
send_file = Response.send_file

View File

@@ -50,7 +50,7 @@ class Microdot(BaseMicrodot):
async def asgi_app(self, scope, receive, send):
"""An ASGI application."""
if scope['type'] != 'http': # pragma: no cover
if scope['type'] not in ['http', 'websocket']: # pragma: no cover
return
path = scope['path']
if 'query_string' in scope and scope['query_string']:
@@ -62,7 +62,6 @@ class Microdot(BaseMicrodot):
if key.lower() == 'content-length':
content_length = int(value)
body = b''
if content_length and content_length <= Request.max_body_length:
body = b''
more = True
@@ -78,12 +77,13 @@ class Microdot(BaseMicrodot):
req = Request(
self,
(scope['client'][0], scope['client'][1]),
scope['method'],
scope.get('method', 'GET'),
path,
'HTTP/' + scope['http_version'],
headers,
body=body,
stream=stream)
stream=stream,
sock=(receive, send))
req.asgi_scope = scope
res = await self.dispatch_request(req)
@@ -97,6 +97,9 @@ class Microdot(BaseMicrodot):
for v in value:
header_list.append((name, v))
if scope['type'] != 'http': # pragma: no cover
return
await send({'type': 'http.response.start',
'status': res.status_code,
'headers': header_list})

View File

@@ -0,0 +1,86 @@
from microdot_asyncio import Response, abort
from microdot_websocket import WebSocket as BaseWebSocket
class WebSocket(BaseWebSocket):
async def handshake(self):
connect = await self.request.sock[0]()
if connect['type'] != 'websocket.connect':
abort(400)
await self.request.sock[1]({'type': 'websocket.accept'})
async def receive(self):
message = await self.request.sock[0]()
if message['type'] == 'websocket.disconnect':
raise OSError(32, 'Websocket connection closed')
elif message['type'] != 'websocket.receive':
raise OSError(32, 'Websocket message type not supported')
return message.get('bytes', message.get('text'))
async def send(self, data):
if isinstance(data, str):
await self.request.sock[1](
{'type': 'websocket.send', 'text': data})
else:
await self.request.sock[1](
{'type': 'websocket.send', 'bytes': data})
async def close(self):
if not self.closed:
self.closed = True
try:
await self.request.sock[1]({'type': 'websocket.close'})
except: # noqa E722
pass
async def websocket_upgrade(request):
"""Upgrade a request handler to a websocket connection.
This function can be called directly inside a route function to process a
WebSocket upgrade handshake, for example after the user's credentials are
verified. The function returns the websocket object::
@app.route('/echo')
async def echo(request):
if not (await authenticate_user(request)):
abort(401)
ws = await websocket_upgrade(request)
while True:
message = await ws.receive()
await ws.send(message)
"""
ws = WebSocket(request)
await ws.handshake()
@request.after_request
async def after_request(request, response):
return Response.already_handled
return ws
def with_websocket(f):
"""Decorator to make a route a WebSocket endpoint.
This decorator is used to define a route that accepts websocket
connections. The route then receives a websocket object as a second
argument that it can use to send and receive messages::
@app.route('/echo')
@with_websocket
async def echo(request, ws):
while True:
message = await ws.receive()
await ws.send(message)
"""
async def wrapper(request, *args, **kwargs):
ws = await websocket_upgrade(request)
try:
await f(request, ws, *args, **kwargs)
except OSError as exc:
if exc.errno != 32 and exc.errno != 54:
raise
await ws.close()
return ''
return wrapper

View File

@@ -20,6 +20,8 @@ from microdot import Microdot as BaseMicrodot
from microdot import print_exception
from microdot import Request as BaseRequest
from microdot import Response as BaseResponse
from microdot import HTTPException
from microdot import MUTED_SOCKET_ERRORS
def _iscoroutine(coro):
@@ -42,22 +44,30 @@ class _AsyncBytesIO:
async def readuntil(self, separator=b'\n'): # pragma: no cover
return self.stream.readuntil(separator=separator)
async def awrite(self, data): # pragma: no cover
return self.stream.write(data)
async def aclose(self): # pragma: no cover
pass
class Request(BaseRequest):
@staticmethod
async def create(app, client_stream, client_addr):
async def create(app, client_reader, client_writer, client_addr):
"""Create a request object.
:param app: The Microdot application instance.
:param client_stream: An input stream from where the request data can
:param client_reader: An input stream from where the request data can
be read.
:param client_writer: An output stream where the response data can be
written.
:param client_addr: The address of the client, as a tuple.
This method is a coroutine. It returns a newly created ``Request``
object.
"""
# request line
line = (await Request._safe_readline(client_stream)).strip().decode()
line = (await Request._safe_readline(client_reader)).strip().decode()
if not line:
return None
method, url, http_version = line.split()
@@ -68,7 +78,7 @@ class Request(BaseRequest):
content_length = 0
while True:
line = (await Request._safe_readline(
client_stream)).strip().decode()
client_reader)).strip().decode()
if line == '':
break
header, value = line.split(':', 1)
@@ -80,14 +90,15 @@ class Request(BaseRequest):
# body
body = b''
if content_length and content_length <= Request.max_body_length:
body = await client_stream.readexactly(content_length)
body = await client_reader.readexactly(content_length)
stream = None
else:
body = b''
stream = client_stream
stream = client_reader
return Request(app, client_addr, method, url, http_version, headers,
body=body, stream=stream)
body=body, stream=stream,
sock=(client_reader, client_writer))
@property
def stream(self):
@@ -118,31 +129,33 @@ class Response(BaseResponse):
default is "OK" for responses with a 200 status code and
"N/A" for any other status codes.
"""
async def write(self, stream):
self.complete()
# status code
reason = self.reason if self.reason is not None else \
('OK' if self.status_code == 200 else 'N/A')
await stream.awrite('HTTP/1.0 {status_code} {reason}\r\n'.format(
status_code=self.status_code, reason=reason).encode())
# headers
for header, value in self.headers.items():
values = value if isinstance(value, list) else [value]
for value in values:
await stream.awrite('{header}: {value}\r\n'.format(
header=header, value=value).encode())
await stream.awrite(b'\r\n')
# body
try:
# status code
reason = self.reason if self.reason is not None else \
('OK' if self.status_code == 200 else 'N/A')
await stream.awrite('HTTP/1.0 {status_code} {reason}\r\n'.format(
status_code=self.status_code, reason=reason).encode())
# headers
for header, value in self.headers.items():
values = value if isinstance(value, list) else [value]
for value in values:
await stream.awrite('{header}: {value}\r\n'.format(
header=header, value=value).encode())
await stream.awrite(b'\r\n')
# body
async for body in self.body_iter():
if isinstance(body, str): # pragma: no cover
body = body.encode()
await stream.awrite(body)
except OSError as exc: # pragma: no cover
if exc.errno == 32 or exc.args[0] == 'Connection lost':
if exc.errno in MUTED_SOCKET_ERRORS or \
exc.args[0] == 'Connection lost':
pass
else:
raise
@@ -194,7 +207,8 @@ class Response(BaseResponse):
class Microdot(BaseMicrodot):
async def start_server(self, host='0.0.0.0', port=5000, debug=False):
async def start_server(self, host='0.0.0.0', port=5000, debug=False,
ssl=None):
"""Start the Microdot web server as a coroutine. This coroutine does
not normally return, as the server enters an endless listening loop.
The :func:`shutdown` function provides a method for terminating the
@@ -211,6 +225,8 @@ class Microdot(BaseMicrodot):
port 5000.
:param debug: If ``True``, the server logs debugging information. The
default is ``False``.
:param ssl: An ``SSLContext`` instance or ``None`` if the server should
not use TLS. The default is ``None``.
This method is a coroutine.
@@ -253,7 +269,12 @@ class Microdot(BaseMicrodot):
print('Starting async server on {host}:{port}...'.format(
host=host, port=port))
self.server = await asyncio.start_server(serve, host, port)
try:
self.server = await asyncio.start_server(serve, host, port,
ssl=ssl)
except TypeError:
self.server = await asyncio.start_server(serve, host, port)
while True:
try:
await self.server.wait_closed()
@@ -263,7 +284,7 @@ class Microdot(BaseMicrodot):
# wait a bit and try again
await asyncio.sleep(0.1)
def run(self, host='0.0.0.0', port=5000, debug=False):
def run(self, host='0.0.0.0', port=5000, debug=False, ssl=None):
"""Start the web server. This function does not normally return, as
the server enters an endless listening loop. The :func:`shutdown`
function provides a method for terminating the server gracefully.
@@ -279,6 +300,8 @@ class Microdot(BaseMicrodot):
port 5000.
:param debug: If ``True``, the server logs debugging information. The
default is ``False``.
:param ssl: An ``SSLContext`` instance or ``None`` if the server should
not use TLS. The default is ``None``.
Example::
@@ -292,7 +315,8 @@ class Microdot(BaseMicrodot):
app.run(debug=True)
"""
asyncio.run(self.start_server(host=host, port=port, debug=debug))
asyncio.run(self.start_server(host=host, port=port, debug=debug,
ssl=ssl))
def shutdown(self):
self.server.close()
@@ -300,17 +324,18 @@ class Microdot(BaseMicrodot):
async def handle_request(self, reader, writer):
req = None
try:
req = await Request.create(self, reader,
req = await Request.create(self, reader, writer,
writer.get_extra_info('peername'))
except Exception as exc: # pragma: no cover
print_exception(exc)
res = await self.dispatch_request(req)
await res.write(writer)
if res != Response.already_handled: # pragma: no branch
await res.write(writer)
try:
await writer.aclose()
except OSError as exc: # pragma: no cover
if exc.errno == 32: # errno.EPIPE
if exc.errno in MUTED_SOCKET_ERRORS:
pass
else:
raise
@@ -360,6 +385,12 @@ class Microdot(BaseMicrodot):
self.error_handlers[f], req)
else:
res = 'Not found', f
except HTTPException as exc:
print_exception(exc)
if exc.status_code in self.error_handlers:
res = self.error_handlers[exc.status_code](req)
else:
res = exc.reason, exc.status_code
except Exception as exc:
print_exception(exc)
res = None
@@ -393,5 +424,7 @@ class Microdot(BaseMicrodot):
return ret
abort = Microdot.abort
Response.already_handled = Response()
redirect = Response.redirect
send_file = Response.send_file

View File

@@ -1,6 +1,10 @@
from microdot_asyncio import Request, _AsyncBytesIO
from microdot_asyncio import Request, Response, _AsyncBytesIO
from microdot_test_client import TestClient as BaseTestClient, \
TestResponse as BaseTestResponse
try:
from microdot_asyncio_websocket import WebSocket
except: # pragma: no cover # noqa: E722
WebSocket = None
class TestResponse(BaseTestResponse):
@@ -17,7 +21,7 @@ class TestResponse(BaseTestResponse):
async def _initialize_body(self, res):
self.body = b''
async for body in res.body_iter():
async for body in res.body_iter(): # pragma: no branch
if isinstance(body, str):
body = body.encode()
self.body += body
@@ -47,15 +51,24 @@ class TestClient(BaseTestClient):
assert res.status_code == 200
assert res.text == 'Hello, World!'
"""
async def request(self, method, path, headers=None, body=None):
async def request(self, method, path, headers=None, body=None, sock=None):
headers = headers or {}
body, headers = self._process_body(body, headers)
cookies, headers = self._process_cookies(headers)
request_bytes = self._render_request(method, path, headers, body)
if sock:
reader = sock[0]
reader.buffer = request_bytes
writer = sock[1]
else:
reader = _AsyncBytesIO(request_bytes)
writer = _AsyncBytesIO(b'')
req = await Request.create(self.app, _AsyncBytesIO(request_bytes),
req = await Request.create(self.app, reader, writer,
('127.0.0.1', 1234))
res = await self.app.dispatch_request(req)
if res == Response.already_handled:
return None
res.complete()
self._update_cookies(res)
@@ -124,3 +137,72 @@ class TestClient(BaseTestClient):
:class:`TestResponse <microdot_test_client.TestResponse>` object.
"""
return await self.request('DELETE', path, headers=headers)
async def websocket(self, path, client, headers=None):
"""Send a websocket connection request to the application.
:param path: The request URL.
:param client: A generator function that yields client messages.
:param headers: A dictionary of headers to send with the request.
"""
gen = client()
class FakeWebSocket:
def __init__(self):
self.started = False
self.closed = False
self.buffer = b''
async def _next(self, data=None):
try:
data = (await gen.asend(data)) if hasattr(gen, 'asend') \
else gen.send(data)
except (StopIteration, StopAsyncIteration):
if not self.closed:
self.closed = True
raise OSError(32, 'Websocket connection closed')
return # pragma: no cover
opcode = WebSocket.TEXT if isinstance(data, str) \
else WebSocket.BINARY
return WebSocket._encode_websocket_frame(opcode, data)
async def read(self, n):
if not self.buffer:
self.started = True
self.buffer = await self._next()
data = self.buffer[:n]
self.buffer = self.buffer[n:]
return data
async def readexactly(self, n): # pragma: no cover
return await self.read(n)
async def readline(self):
line = b''
while True:
line += await self.read(1)
if line[-1] in [b'\n', 10]:
break
return line
async def awrite(self, data):
if self.started:
h = WebSocket._parse_frame_header(data[0:2])
if h[3] < 0:
data = data[2 - h[3]:]
else:
data = data[2:]
if h[1] == WebSocket.TEXT:
data = data.decode()
self.buffer = await self._next(data)
ws_headers = {
'Upgrade': 'websocket',
'Connection': 'Upgrade',
'Sec-WebSocket-Version': '13',
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
}
ws_headers.update(headers or {})
sock = FakeWebSocket()
return await self.request('GET', path, headers=ws_headers,
sock=(sock, sock))

View File

@@ -0,0 +1,103 @@
from microdot_asyncio import Response
from microdot_websocket import WebSocket as BaseWebSocket
class WebSocket(BaseWebSocket):
async def handshake(self):
response = self._handshake_response()
await self.request.sock[1].awrite(
b'HTTP/1.1 101 Switching Protocols\r\n')
await self.request.sock[1].awrite(b'Upgrade: websocket\r\n')
await self.request.sock[1].awrite(b'Connection: Upgrade\r\n')
await self.request.sock[1].awrite(
b'Sec-WebSocket-Accept: ' + response + b'\r\n\r\n')
async def receive(self):
while True:
opcode, payload = await self._read_frame()
send_opcode, data = self._process_websocket_frame(opcode, payload)
if send_opcode: # pragma: no cover
await self.send(send_opcode, data)
elif data: # pragma: no branch
return data
async def send(self, data, opcode=None):
frame = self._encode_websocket_frame(
opcode or (self.TEXT if isinstance(data, str) else self.BINARY),
data)
await self.request.sock[1].awrite(frame)
async def close(self):
if not self.closed: # pragma: no cover
self.closed = True
await self.send(b'', self.CLOSE)
async def _read_frame(self):
header = await self.request.sock[0].read(2)
if len(header) != 2: # pragma: no cover
raise OSError(32, 'Websocket connection closed')
fin, opcode, has_mask, length = self._parse_frame_header(header)
if length == -2:
length = await self.request.sock[0].read(2)
length = int.from_bytes(length, 'big')
elif length == -8:
length = await self.request.sock[0].read(8)
length = int.from_bytes(length, 'big')
if has_mask: # pragma: no cover
mask = await self.request.sock[0].read(4)
payload = await self.request.sock[0].read(length)
if has_mask: # pragma: no cover
payload = bytes(x ^ mask[i % 4] for i, x in enumerate(payload))
return opcode, payload
async def websocket_upgrade(request):
"""Upgrade a request handler to a websocket connection.
This function can be called directly inside a route function to process a
WebSocket upgrade handshake, for example after the user's credentials are
verified. The function returns the websocket object::
@app.route('/echo')
async def echo(request):
if not authenticate_user(request):
abort(401)
ws = await websocket_upgrade(request)
while True:
message = await ws.receive()
await ws.send(message)
"""
ws = WebSocket(request)
await ws.handshake()
@request.after_request
async def after_request(request, response):
return Response.already_handled
return ws
def with_websocket(f):
"""Decorator to make a route a WebSocket endpoint.
This decorator is used to define a route that accepts websocket
connections. The route then receives a websocket object as a second
argument that it can use to send and receive messages::
@app.route('/echo')
@with_websocket
async def echo(request, ws):
while True:
message = await ws.receive()
await ws.send(message)
"""
async def wrapper(request, *args, **kwargs):
ws = await websocket_upgrade(request)
try:
await f(request, ws, *args, **kwargs)
await ws.close() # pragma: no cover
except OSError as exc:
if exc.errno not in [32, 54, 104]: # pragma: no cover
raise
return ''
return wrapper

61
src/microdot_ssl.py Normal file
View File

@@ -0,0 +1,61 @@
import ssl
def create_ssl_context(cert, key, **kwargs):
"""Create an SSL context to wrap sockets with.
:param cert: The certificate to use. If it is given as a string, it is
assumed to be a filename. If it is given as a bytes object, it
is assumed to be the certificate data. In both cases the data
is expected to be in PEM format for CPython and in DER format
for MicroPython.
:param key: The private key to use. If it is given as a string, it is
assumed to be a filename. If it is given as a bytes object, it
is assumed to be the private key data. in both cases the data
is expected to be in PEM format for CPython and in DER format
for MicroPython.
:param kwargs: Additional arguments to pass to the ``ssl.wrap_socket``
function.
Note: This function creates a fairly limited SSL context object to enable
the use of certificates under MicroPython. It is not intended to be used in
any other context, and in particular, it is not needed when using CPython
or any other Python implementation that has native support for
``SSLContext`` objects. Once MicroPython implements ``SSLContext``
natively, this function will be deprecated.
"""
if hasattr(ssl, 'SSLContext'):
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER, **kwargs)
ctx.load_cert_chain(cert, key)
return ctx
if isinstance(cert, str):
with open(cert, 'rb') as f:
cert = f.read()
if isinstance(key, str):
with open(key, 'rb') as f:
key = f.read()
class FakeSSLSocket:
def __init__(self, sock, **kwargs):
self.sock = sock
self.kwargs = kwargs
def accept(self):
client, addr = self.sock.accept()
return (ssl.wrap_socket(client, cert=cert, key=key, **self.kwargs),
addr)
def close(self):
self.sock.close()
class FakeSSLContext:
def __init__(self, **kwargs):
self.kwargs = kwargs
def wrap_socket(self, sock, **kwargs):
all_kwargs = self.kwargs.copy()
all_kwargs.update(kwargs)
return FakeSSLSocket(sock, **all_kwargs)
return FakeSSLContext(**kwargs)

View File

@@ -1,6 +1,10 @@
from io import BytesIO
import json
from microdot import Request
from microdot import Request, Response
try:
from microdot_websocket import WebSocket
except: # pragma: no cover # noqa: E722
WebSocket = None
class TestResponse:
@@ -44,7 +48,7 @@ class TestResponse:
def _process_json_body(self):
for name, value in self.headers.items(): # pragma: no branch
if name.lower() == 'content-type':
if value.lower() == 'application/json':
if value.lower().split(';')[0] == 'application/json':
self.json = json.loads(self.text)
break
@@ -82,6 +86,8 @@ class TestClient:
assert res.status_code == 200
assert res.text == 'Hello, World!'
"""
__test__ = False # remove this class from pytest's test collection
def __init__(self, app, cookies=None):
self.app = app
self.cookies = cookies or {}
@@ -147,15 +153,17 @@ class TestClient:
else:
self.cookies[cookie_name] = cookie_options[0]
def request(self, method, path, headers=None, body=None):
def request(self, method, path, headers=None, body=None, sock=None):
headers = headers or {}
body, headers = self._process_body(body, headers)
cookies, headers = self._process_cookies(headers)
request_bytes = self._render_request(method, path, headers, body)
req = Request.create(self.app, BytesIO(request_bytes),
('127.0.0.1', 1234))
('127.0.0.1', 1234), client_sock=sock)
res = self.app.dispatch_request(req)
if res == Response.already_handled:
return None
res.complete()
self._update_cookies(res)
@@ -224,3 +232,59 @@ class TestClient:
:class:`TestResponse <microdot_test_client.TestResponse>` object.
"""
return self.request('DELETE', path, headers=headers)
def websocket(self, path, client, headers=None):
"""Send a websocket connection request to the application.
:param path: The request URL.
:param client: A generator function that yields client messages.
:param headers: A dictionary of headers to send with the request.
"""
gen = client()
class FakeWebSocket:
def __init__(self):
self.started = False
self.closed = False
self.buffer = b''
def _next(self, data=None):
try:
data = gen.send(data)
except StopIteration:
if self.closed: # pragma: no cover
return
self.closed = True
raise OSError(32, 'Websocket connection closed')
opcode = WebSocket.TEXT if isinstance(data, str) \
else WebSocket.BINARY
return WebSocket._encode_websocket_frame(opcode, data)
def read(self, n):
self.started = True
if not self.buffer:
self.buffer = self._next()
data = self.buffer[:n]
self.buffer = self.buffer[n:]
return data
def write(self, data):
if self.started:
h = WebSocket._parse_frame_header(data[0:2])
if h[3] < 0:
data = data[2 - h[3]:]
else:
data = data[2:]
if h[1] == WebSocket.TEXT:
data = data.decode()
self.buffer = self._next(data)
ws_headers = {
'Upgrade': 'websocket',
'Connection': 'Upgrade',
'Sec-WebSocket-Version': '13',
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
}
ws_headers.update(headers or {})
return self.request('GET', path, headers=ws_headers,
sock=FakeWebSocket())

177
src/microdot_websocket.py Normal file
View File

@@ -0,0 +1,177 @@
import binascii
import hashlib
from microdot import Response
class WebSocket:
CONT = 0
TEXT = 1
BINARY = 2
CLOSE = 8
PING = 9
PONG = 10
def __init__(self, request):
self.request = request
self.closed = False
def handshake(self):
response = self._handshake_response()
self.request.sock.write(b'HTTP/1.1 101 Switching Protocols\r\n')
self.request.sock.write(b'Upgrade: websocket\r\n')
self.request.sock.write(b'Connection: Upgrade\r\n')
self.request.sock.write(
b'Sec-WebSocket-Accept: ' + response + b'\r\n\r\n')
def receive(self):
while True:
opcode, payload = self._read_frame()
send_opcode, data = self._process_websocket_frame(opcode, payload)
if send_opcode: # pragma: no cover
self.send(send_opcode, data)
elif data: # pragma: no branch
return data
def send(self, data, opcode=None):
frame = self._encode_websocket_frame(
opcode or (self.TEXT if isinstance(data, str) else self.BINARY),
data)
self.request.sock.write(frame)
def close(self):
if not self.closed: # pragma: no cover
self.closed = True
self.send(b'', self.CLOSE)
def _handshake_response(self):
connection = False
upgrade = False
websocket_key = None
for header, value in self.request.headers.items():
h = header.lower()
if h == 'connection':
connection = True
if 'upgrade' not in value.lower():
return self.request.app.abort(400)
elif h == 'upgrade':
upgrade = True
if not value.lower() == 'websocket':
return self.request.app.abort(400)
elif h == 'sec-websocket-key':
websocket_key = value
if not connection or not upgrade or not websocket_key:
return self.request.app.abort(400)
d = hashlib.sha1(websocket_key.encode())
d.update(b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
return binascii.b2a_base64(d.digest())[:-1]
@classmethod
def _parse_frame_header(cls, header):
fin = header[0] & 0x80
opcode = header[0] & 0x0f
if fin == 0 or opcode == cls.CONT: # pragma: no cover
raise OSError(32, 'Continuation frames not supported')
has_mask = header[1] & 0x80
length = header[1] & 0x7f
if length == 126:
length = -2
elif length == 127:
length = -8
return fin, opcode, has_mask, length
def _process_websocket_frame(self, opcode, payload):
if opcode == self.TEXT:
payload = payload.decode()
elif opcode == self.BINARY:
pass
elif opcode == self.CLOSE:
raise OSError(32, 'Websocket connection closed')
elif opcode == self.PING:
return self.PONG, payload
elif opcode == self.PONG: # pragma: no branch
return None, None
return None, payload
@classmethod
def _encode_websocket_frame(cls, opcode, payload):
frame = bytearray()
frame.append(0x80 | opcode)
if opcode == cls.TEXT:
payload = payload.encode()
if len(payload) < 126:
frame.append(len(payload))
elif len(payload) < (1 << 16):
frame.append(126)
frame.extend(len(payload).to_bytes(2, 'big'))
else:
frame.append(127)
frame.extend(len(payload).to_bytes(8, 'big'))
frame.extend(payload)
return frame
def _read_frame(self):
header = self.request.sock.read(2)
if len(header) != 2: # pragma: no cover
raise OSError(32, 'Websocket connection closed')
fin, opcode, has_mask, length = self._parse_frame_header(header)
if length < 0:
length = self.request.sock.read(-length)
length = int.from_bytes(length, 'big')
if has_mask: # pragma: no cover
mask = self.request.sock.read(4)
payload = self.request.sock.read(length)
if has_mask: # pragma: no cover
payload = bytes(x ^ mask[i % 4] for i, x in enumerate(payload))
return opcode, payload
def websocket_upgrade(request):
"""Upgrade a request handler to a websocket connection.
This function can be called directly inside a route function to process a
WebSocket upgrade handshake, for example after the user's credentials are
verified. The function returns the websocket object::
@app.route('/echo')
def echo(request):
if not authenticate_user(request):
abort(401)
ws = websocket_upgrade(request)
while True:
message = ws.receive()
ws.send(message)
"""
ws = WebSocket(request)
ws.handshake()
@request.after_request
def after_request(request, response):
return Response.already_handled
return ws
def with_websocket(f):
"""Decorator to make a route a WebSocket endpoint.
This decorator is used to define a route that accepts websocket
connections. The route then receives a websocket object as a second
argument that it can use to send and receive messages::
@app.route('/echo')
@with_websocket
def echo(request, ws):
while True:
message = ws.receive()
ws.send(message)
"""
def wrapper(request, *args, **kwargs):
ws = websocket_upgrade(request)
try:
f(request, ws, *args, **kwargs)
ws.close() # pragma: no cover
except OSError as exc:
if exc.errno not in [32, 54, 104]: # pragma: no cover
raise
return ''
return wrapper

View File

@@ -0,0 +1,114 @@
import binascii
import hashlib
import select
import websocket as _websocket
from microdot import Response
class WebSocket:
CONT = 0
TEXT = 1
BINARY = 2
CLOSE = 8
PING = 9
PONG = 10
def __init__(self, request):
self.request = request
self.poll = select.poll()
self.poll.register(self.request.sock, select.POLLIN)
self.ws = _websocket.websocket(self.request.sock, True)
self.request.sock.setblocking(False)
def handshake(self):
response = self._handshake_response()
self.request.sock.write(b'HTTP/1.1 101 Switching Protocols\r\n')
self.request.sock.write(b'Upgrade: websocket\r\n')
self.request.sock.write(b'Connection: Upgrade\r\n')
self.request.sock.write(
b'Sec-WebSocket-Accept: ' + response + b'\r\n\r\n')
def receive(self):
while True:
self.poll.poll()
data = self.ws.read()
if data:
try:
data = data.decode()
except ValueError:
pass
return data
def send(self, data):
self.ws.write(data)
def close(self):
self.poll.unregister(self.request.sock)
self.ws.close()
def _handshake_response(self):
for header, value in self.request.headers.items():
h = header.lower()
if h == 'connection' and not value.lower().startswith('upgrade'):
return self.request.app.abort(400)
elif h == 'upgrade' and not value.lower() == 'websocket':
return self.request.app.abort(400)
elif h == 'sec-websocket-key':
websocket_key = value
if not websocket_key:
return self.request.app.abort(400)
d = hashlib.sha1(websocket_key.encode())
d.update(b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
return binascii.b2a_base64(d.digest())[:-1]
def websocket_upgrade(request):
"""Upgrade a request handler to a websocket connection.
This function can be called directly inside a route function to process a
WebSocket upgrade handshake, for example after the user's credentials are
verified. The function returns the websocket object::
@app.route('/echo')
def echo(request):
if not authenticate_user(request):
abort(401)
ws = websocket_upgrade(request)
while True:
message = ws.receive()
ws.send(message)
"""
ws = WebSocket(request)
ws.handshake()
@request.after_request
def after_request(request, response):
return Response.already_handled
return ws
def with_websocket(f):
"""Decorator to make a route a WebSocket endpoint.
This decorator is used to define a route that accepts websocket
connections. The route then receives a websocket object as a second
argument that it can use to send and receive messages::
@app.route('/echo')
@with_websocket
def echo(request, ws):
while True:
message = ws.receive()
ws.send(message)
"""
def wrapper(request, *args, **kwargs):
ws = websocket_upgrade(request)
try:
f(request, ws, *args, **kwargs)
except OSError as exc:
if exc.errno != 32 and exc.errno != 54:
raise
ws.close()
return ''
return wrapper

View File

@@ -27,7 +27,8 @@ class Microdot(BaseMicrodot):
path,
environ['SERVER_PROTOCOL'],
headers,
stream=environ['wsgi.input'])
stream=environ['wsgi.input'],
sock=environ.get('gunicorn.socket'))
req.environ = environ
res = self.dispatch_request(req)

View File

@@ -3,11 +3,12 @@ from .test_request import TestRequest
from .test_response import TestResponse
from .test_url_pattern import TestURLPattern
from .test_microdot import TestMicrodot
from .test_microdot_websocket import TestMicrodotWebSocket
from .test_request_asyncio import TestRequestAsync
from .test_response_asyncio import TestResponseAsync
from .test_microdot_asyncio import TestMicrodotAsync
from .test_microdot_asyncio_websocket import TestMicrodotAsyncWebSocket
from .test_utemplate import TestUTemplate
from .test_session import TestSession

View File

@@ -19,7 +19,7 @@ def _run(coro):
@unittest.skipIf(sys.implementation.name == 'micropython',
'not supported under MicroPython')
class TestUTemplate(unittest.TestCase):
class TestJinja(unittest.TestCase):
def test_render_template(self):
s = render_template('hello.jinja.txt', name='foo')
self.assertEqual(s, 'Hello, foo!')
@@ -44,7 +44,7 @@ class TestUTemplate(unittest.TestCase):
return render_template('hello.jinja.txt', name='foo')
req = _run(RequestAsync.create(
app, get_async_request_fd('GET', '/'), 'addr'))
app, get_async_request_fd('GET', '/'), 'writer', 'addr'))
res = _run(app.dispatch_request(req))
self.assertEqual(res.status_code, 200)

View File

@@ -1,6 +1,6 @@
import sys
import unittest
from microdot import Microdot, Response
from microdot import Microdot, Response, abort
from microdot_test_client import TestClient
from tests import mock_socket
@@ -37,7 +37,8 @@ class TestMicrodot(unittest.TestCase):
client = TestClient(app)
res = client.get('/')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.headers['Content-Length'], '3')
self.assertEqual(res.text, 'foo')
self.assertEqual(res.body, b'foo')
@@ -57,7 +58,8 @@ class TestMicrodot(unittest.TestCase):
client = TestClient(app)
res = client.post('/')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.headers['Content-Length'], '3')
self.assertEqual(res.text, 'bar')
@@ -73,7 +75,8 @@ class TestMicrodot(unittest.TestCase):
app.run()
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.assertIn(b'Content-Type: text/plain; charset=UTF-8\r\n',
fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nBad request'))
self._unmock()
@@ -106,7 +109,8 @@ class TestMicrodot(unittest.TestCase):
for method in methods:
res = getattr(client, method.lower())('/' + method.lower())
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, method)
def test_headers(self):
@@ -119,7 +123,8 @@ class TestMicrodot(unittest.TestCase):
client = TestClient(app)
res = client.get('/', headers={'X-Foo': 'bar'})
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'bar')
def test_cookies(self):
@@ -133,7 +138,8 @@ class TestMicrodot(unittest.TestCase):
client = TestClient(app, cookies={'one': '1', 'two': '2'})
res = client.get('/', headers={'Cookie': 'three=3'})
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, '123')
def test_binary_payload(self):
@@ -146,7 +152,8 @@ class TestMicrodot(unittest.TestCase):
client = TestClient(app)
res = client.post('/', body=b'foo')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'foo')
def test_json_payload(self):
@@ -165,12 +172,14 @@ class TestMicrodot(unittest.TestCase):
res = client.post('/dict', body={'foo': 'bar'})
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'bar')
res = client.post('/list', body=['foo', 'bar'])
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'foo')
def test_tuple_responses(self):
@@ -190,18 +199,21 @@ class TestMicrodot(unittest.TestCase):
@app.route('/body-status-headers')
def four(req):
return '<p>four</p>', 202, {'Content-Type': 'text/html'}
return '<p>four</p>', 202, \
{'Content-Type': 'text/html; charset=UTF-8'}
client = TestClient(app)
res = client.get('/body')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'one')
res = client.get('/body-status')
self.assertEqual(res.status_code, 202)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'two')
res = client.get('/body-headers')
@@ -211,7 +223,8 @@ class TestMicrodot(unittest.TestCase):
res = client.get('/body-status-headers')
self.assertEqual(res.status_code, 202)
self.assertEqual(res.headers['Content-Type'], 'text/html')
self.assertEqual(res.headers['Content-Type'],
'text/html; charset=UTF-8')
self.assertEqual(res.text, '<p>four</p>')
def test_before_after_request(self):
@@ -248,7 +261,8 @@ class TestMicrodot(unittest.TestCase):
res = client.get('/bar')
self.assertEqual(res.status_code, 202)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.headers['Set-Cookie'], ['foo=bar'])
self.assertEqual(res.headers['X-One'], '1')
self.assertEqual(res.headers['X-Two'], '2')
@@ -257,7 +271,8 @@ class TestMicrodot(unittest.TestCase):
res = client.get('/baz')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.headers['Set-Cookie'], ['foo=bar'])
self.assertEqual(res.headers['X-One'], '1')
self.assertFalse('X-Two' in res.headers)
@@ -276,7 +291,8 @@ class TestMicrodot(unittest.TestCase):
app.run()
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.assertIn(b'Content-Type: text/plain; charset=UTF-8\r\n',
fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nBad request'))
self._unmock()
@@ -297,7 +313,8 @@ class TestMicrodot(unittest.TestCase):
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 200 OK\r\n'))
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain; charset=UTF-8\r\n',
fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\n400'))
self._unmock()
@@ -312,7 +329,8 @@ class TestMicrodot(unittest.TestCase):
client = TestClient(app)
res = client.post('/foo')
self.assertEqual(res.status_code, 404)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'Not found')
def test_404_handler(self):
@@ -329,7 +347,8 @@ class TestMicrodot(unittest.TestCase):
client = TestClient(app)
res = client.post('/foo')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, '404')
def test_405(self):
@@ -342,7 +361,8 @@ class TestMicrodot(unittest.TestCase):
client = TestClient(app)
res = client.post('/foo')
self.assertEqual(res.status_code, 405)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'Not found')
def test_405_handler(self):
@@ -359,7 +379,8 @@ class TestMicrodot(unittest.TestCase):
client = TestClient(app)
res = client.patch('/foo')
self.assertEqual(res.status_code, 405)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, '405')
def test_413(self):
@@ -372,7 +393,8 @@ class TestMicrodot(unittest.TestCase):
client = TestClient(app)
res = client.post('/foo', body='x' * 17000)
self.assertEqual(res.status_code, 413)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'Payload too large')
def test_413_handler(self):
@@ -389,7 +411,8 @@ class TestMicrodot(unittest.TestCase):
client = TestClient(app)
res = client.post('/foo', body='x' * 17000)
self.assertEqual(res.status_code, 400)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, '413')
def test_500(self):
@@ -402,7 +425,8 @@ class TestMicrodot(unittest.TestCase):
client = TestClient(app)
res = client.get('/')
self.assertEqual(res.status_code, 500)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'Internal server error')
def test_500_handler(self):
@@ -419,7 +443,8 @@ class TestMicrodot(unittest.TestCase):
client = TestClient(app)
res = client.get('/')
self.assertEqual(res.status_code, 501)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, '501')
def test_exception_handler(self):
@@ -436,9 +461,44 @@ class TestMicrodot(unittest.TestCase):
client = TestClient(app)
res = client.get('/')
self.assertEqual(res.status_code, 501)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, '501')
def test_abort(self):
app = Microdot()
@app.route('/')
def index(req):
abort(406, 'Not acceptable')
return 'foo'
client = TestClient(app)
res = client.get('/')
self.assertEqual(res.status_code, 406)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'Not acceptable')
def test_abort_handler(self):
app = Microdot()
@app.route('/')
def index(req):
abort(406)
return 'foo'
@app.errorhandler(406)
def handle_406(req):
return '406', 406
client = TestClient(app)
res = client.get('/')
self.assertEqual(res.status_code, 406)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, '406')
def test_json_response(self):
app = Microdot()
@@ -454,12 +514,14 @@ class TestMicrodot(unittest.TestCase):
res = client.get('/dict')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'application/json')
self.assertEqual(res.headers['Content-Type'],
'application/json; charset=UTF-8')
self.assertEqual(res.json, {'foo': 'bar'})
res = client.get('/list')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'application/json')
self.assertEqual(res.headers['Content-Type'],
'application/json; charset=UTF-8')
self.assertEqual(res.json, ['foo', 'bar'])
def test_binary_response(self):
@@ -491,9 +553,21 @@ class TestMicrodot(unittest.TestCase):
client = TestClient(app)
res = client.get('/')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'foobar')
def test_already_handled_response(self):
app = Microdot()
@app.route('/')
def index(req):
return Response.already_handled
client = TestClient(app)
res = client.get('/')
self.assertEqual(res, None)
def test_mount(self):
subapp = Microdot()
@@ -520,10 +594,37 @@ class TestMicrodot(unittest.TestCase):
res = client.get('/app')
self.assertEqual(res.status_code, 404)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, '404')
res = client.get('/sub/app')
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'before:foo:after')
def test_ssl(self):
self._mock()
app = Microdot()
@app.route('/foo')
def foo(req):
return 'bar'
class FakeSSL:
def wrap_socket(self, sock, **kwargs):
return sock
mock_socket.clear_requests()
fd = mock_socket.add_request('GET', '/foo')
self._add_shutdown(app)
app.run(ssl=FakeSSL())
self.assertTrue(fd.response.startswith(b'HTTP/1.0 200 OK\r\n'))
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain; charset=UTF-8\r\n',
fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nbar'))
self._unmock()

View File

@@ -82,10 +82,12 @@ class TestMicrodotASGI(unittest.TestCase):
async def send(packet):
if packet['type'] == 'http.response.start':
self.assertEqual(packet['status'], 200)
expected_headers = [('Content-Length', '8'),
('Content-Type', 'text/plain'),
('Set-Cookie', 'foo=foo'),
('Set-Cookie', 'bar=bar; HttpOnly')]
expected_headers = [
('Content-Length', '8'),
('Content-Type', 'text/plain; charset=UTF-8'),
('Set-Cookie', 'foo=foo'),
('Set-Cookie', 'bar=bar; HttpOnly')
]
self.assertEqual(len(packet['headers']), len(expected_headers))
for header in expected_headers:
self.assertIn(header, packet['headers'])

View File

@@ -4,7 +4,7 @@ except ImportError:
import asyncio
import sys
import unittest
from microdot_asyncio import Microdot, Response
from microdot_asyncio import Microdot, Response, abort
from microdot_asyncio_test_client import TestClient
from tests import mock_asyncio, mock_socket
@@ -50,7 +50,8 @@ class TestMicrodotAsync(unittest.TestCase):
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.headers['Content-Length'], '3')
self.assertEqual(res.text, 'foo')
self.assertEqual(res.body, b'foo')
@@ -58,7 +59,8 @@ class TestMicrodotAsync(unittest.TestCase):
res = self._run(client.get('/async'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.headers['Content-Length'], '9')
self.assertEqual(res.text, 'foo-async')
self.assertEqual(res.body, b'foo-async')
@@ -83,7 +85,8 @@ class TestMicrodotAsync(unittest.TestCase):
res = self._run(client.post('/'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.headers['Content-Length'], '3')
self.assertEqual(res.text, 'bar')
self.assertEqual(res.body, b'bar')
@@ -91,7 +94,8 @@ class TestMicrodotAsync(unittest.TestCase):
res = self._run(client.post('/async'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.headers['Content-Length'], '9')
self.assertEqual(res.text, 'bar-async')
self.assertEqual(res.body, b'bar-async')
@@ -107,7 +111,8 @@ class TestMicrodotAsync(unittest.TestCase):
app.run()
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.assertIn(b'Content-Type: text/plain; charset=UTF-8\r\n',
fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nBad request'))
def test_method_decorators(self):
@@ -139,7 +144,8 @@ class TestMicrodotAsync(unittest.TestCase):
res = self._run(getattr(
client, method.lower())('/' + method.lower()))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, method)
def test_headers(self):
@@ -152,7 +158,8 @@ class TestMicrodotAsync(unittest.TestCase):
client = TestClient(app)
res = self._run(client.get('/', headers={'X-Foo': 'bar'}))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'bar')
def test_cookies(self):
@@ -166,7 +173,8 @@ class TestMicrodotAsync(unittest.TestCase):
client = TestClient(app, cookies={'one': '1', 'two': '2'})
res = self._run(client.get('/', headers={'Cookie': 'three=3'}))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, '123')
def test_binary_payload(self):
@@ -179,7 +187,8 @@ class TestMicrodotAsync(unittest.TestCase):
client = TestClient(app)
res = self._run(client.post('/', body=b'foo'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'foo')
def test_json_payload(self):
@@ -198,12 +207,14 @@ class TestMicrodotAsync(unittest.TestCase):
res = self._run(client.post('/dict', body={'foo': 'bar'}))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'bar')
res = self._run(client.post('/list', body=['foo', 'bar']))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'foo')
def test_tuple_responses(self):
@@ -223,18 +234,21 @@ class TestMicrodotAsync(unittest.TestCase):
@app.route('/body-status-headers')
def four(req):
return '<p>four</p>', 202, {'Content-Type': 'text/html'}
return '<p>four</p>', 202, \
{'Content-Type': 'text/html; charset=UTF-8'}
client = TestClient(app)
res = self._run(client.get('/body'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'one')
res = self._run(client.get('/body-status'))
self.assertEqual(res.status_code, 202)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'two')
res = self._run(client.get('/body-headers'))
@@ -244,7 +258,8 @@ class TestMicrodotAsync(unittest.TestCase):
res = self._run(client.get('/body-status-headers'))
self.assertEqual(res.status_code, 202)
self.assertEqual(res.headers['Content-Type'], 'text/html')
self.assertEqual(res.headers['Content-Type'],
'text/html; charset=UTF-8')
self.assertEqual(res.text, '<p>four</p>')
def test_before_after_request(self):
@@ -281,7 +296,8 @@ class TestMicrodotAsync(unittest.TestCase):
res = self._run(client.get('/bar'))
self.assertEqual(res.status_code, 202)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.headers['Set-Cookie'], ['foo=bar'])
self.assertEqual(res.headers['X-One'], '1')
self.assertEqual(res.headers['X-Two'], '2')
@@ -290,7 +306,8 @@ class TestMicrodotAsync(unittest.TestCase):
res = self._run(client.get('/baz'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.headers['Set-Cookie'], ['foo=bar'])
self.assertEqual(res.headers['X-One'], '1')
self.assertFalse('X-Two' in res.headers)
@@ -309,7 +326,8 @@ class TestMicrodotAsync(unittest.TestCase):
app.run()
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.assertIn(b'Content-Type: text/plain; charset=UTF-8\r\n',
fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nBad request'))
self._unmock()
@@ -330,7 +348,8 @@ class TestMicrodotAsync(unittest.TestCase):
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 200 OK\r\n'))
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain; charset=UTF-8\r\n',
fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\n400'))
self._unmock()
@@ -345,7 +364,8 @@ class TestMicrodotAsync(unittest.TestCase):
client = TestClient(app)
res = self._run(client.post('/foo'))
self.assertEqual(res.status_code, 404)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'Not found')
def test_404_handler(self):
@@ -362,7 +382,8 @@ class TestMicrodotAsync(unittest.TestCase):
client = TestClient(app)
res = self._run(client.post('/foo'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, '404')
def test_405(self):
@@ -375,7 +396,8 @@ class TestMicrodotAsync(unittest.TestCase):
client = TestClient(app)
res = self._run(client.post('/foo'))
self.assertEqual(res.status_code, 405)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'Not found')
def test_405_handler(self):
@@ -392,7 +414,8 @@ class TestMicrodotAsync(unittest.TestCase):
client = TestClient(app)
res = self._run(client.patch('/foo'))
self.assertEqual(res.status_code, 405)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, '405')
def test_413(self):
@@ -405,7 +428,8 @@ class TestMicrodotAsync(unittest.TestCase):
client = TestClient(app)
res = self._run(client.post('/foo', body='x' * 17000))
self.assertEqual(res.status_code, 413)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'Payload too large')
def test_413_handler(self):
@@ -422,7 +446,8 @@ class TestMicrodotAsync(unittest.TestCase):
client = TestClient(app)
res = self._run(client.post('/foo', body='x' * 17000))
self.assertEqual(res.status_code, 400)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, '413')
def test_500(self):
@@ -435,7 +460,8 @@ class TestMicrodotAsync(unittest.TestCase):
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 500)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'Internal server error')
def test_500_handler(self):
@@ -452,7 +478,8 @@ class TestMicrodotAsync(unittest.TestCase):
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 501)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, '501')
def test_exception_handler(self):
@@ -469,9 +496,44 @@ class TestMicrodotAsync(unittest.TestCase):
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 501)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, '501')
def test_abort(self):
app = Microdot()
@app.route('/')
def index(req):
abort(406, 'Not acceptable')
return 'foo'
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 406)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'Not acceptable')
def test_abort_handler(self):
app = Microdot()
@app.route('/')
def index(req):
abort(406)
return 'foo'
@app.errorhandler(406)
def handle_500(req):
return '406', 406
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 406)
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, '406')
def test_json_response(self):
app = Microdot()
@@ -487,12 +549,14 @@ class TestMicrodotAsync(unittest.TestCase):
res = self._run(client.get('/dict'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'application/json')
self.assertEqual(res.headers['Content-Type'],
'application/json; charset=UTF-8')
self.assertEqual(res.json, {'foo': 'bar'})
res = self._run(client.get('/list'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'application/json')
self.assertEqual(res.headers['Content-Type'],
'application/json; charset=UTF-8')
self.assertEqual(res.json, ['foo', 'bar'])
def test_binary_response(self):
@@ -536,5 +600,17 @@ class TestMicrodotAsync(unittest.TestCase):
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers['Content-Type'], 'text/plain')
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
self.assertEqual(res.text, 'foobar')
def test_already_handled_response(self):
app = Microdot()
@app.route('/')
def index(req):
return Response.already_handled
client = TestClient(app)
res = self._run(client.get('/'))
self.assertEqual(res, None)

View File

@@ -0,0 +1,71 @@
import sys
try:
import uasyncio as asyncio
except ImportError:
import asyncio
import unittest
from microdot_asyncio import Microdot
from microdot_asyncio_websocket import with_websocket
from microdot_asyncio_test_client import TestClient
class TestMicrodotAsyncWebSocket(unittest.TestCase):
def _run(self, coro):
loop = asyncio.get_event_loop()
return loop.run_until_complete(coro)
def test_websocket_echo(self):
app = Microdot()
@app.route('/echo')
@with_websocket
async def index(req, ws):
while True:
data = await ws.receive()
await ws.send(data)
results = []
def ws():
data = yield 'hello'
results.append(data)
data = yield b'bye'
results.append(data)
data = yield b'*' * 300
results.append(data)
data = yield b'+' * 65537
results.append(data)
client = TestClient(app)
res = self._run(client.websocket('/echo', ws))
self.assertIsNone(res)
self.assertEqual(results, ['hello', b'bye', b'*' * 300, b'+' * 65537])
@unittest.skipIf(sys.implementation.name == 'micropython',
'no support for async generators in MicroPython')
def test_websocket_echo_async_client(self):
app = Microdot()
@app.route('/echo')
@with_websocket
async def index(req, ws):
while True:
data = await ws.receive()
await ws.send(data)
results = []
async def ws():
data = yield 'hello'
results.append(data)
data = yield b'bye'
results.append(data)
data = yield b'*' * 300
results.append(data)
data = yield b'+' * 65537
results.append(data)
client = TestClient(app)
res = self._run(client.websocket('/echo', ws))
self.assertIsNone(res)
self.assertEqual(results, ['hello', b'bye', b'*' * 300, b'+' * 65537])

View File

@@ -0,0 +1,73 @@
import unittest
from microdot import Microdot
from microdot_websocket import with_websocket, WebSocket
from microdot_test_client import TestClient
class TestMicrodotWebSocket(unittest.TestCase):
def test_websocket_echo(self):
app = Microdot()
@app.route('/echo')
@with_websocket
def index(req, ws):
while True:
data = ws.receive()
ws.send(data)
results = []
def ws():
data = yield 'hello'
results.append(data)
data = yield b'bye'
results.append(data)
data = yield b'*' * 300
results.append(data)
data = yield b'+' * 65537
results.append(data)
client = TestClient(app)
res = client.websocket('/echo', ws)
self.assertIsNone(res)
self.assertEqual(results, ['hello', b'bye', b'*' * 300, b'+' * 65537])
def test_bad_websocket_request(self):
app = Microdot()
@app.route('/echo')
@with_websocket
def index(req, ws):
return 'hello'
client = TestClient(app)
res = client.get('/echo')
self.assertEqual(res.status_code, 400)
res = client.get('/echo', headers={'Connection': 'Upgrade'})
self.assertEqual(res.status_code, 400)
res = client.get('/echo', headers={'Connection': 'foo'})
self.assertEqual(res.status_code, 400)
res = client.get('/echo', headers={'Upgrade': 'websocket'})
self.assertEqual(res.status_code, 400)
res = client.get('/echo', headers={'Upgrade': 'bar'})
self.assertEqual(res.status_code, 400)
res = client.get('/echo', headers={'Connection': 'Upgrade',
'Upgrade': 'websocket'})
self.assertEqual(res.status_code, 400)
res = client.get('/echo', headers={'Sec-WebSocket-Key': 'xxx'})
self.assertEqual(res.status_code, 400)
def test_process_websocket_frame(self):
ws = WebSocket(None)
ws.closed = True
self.assertEqual(ws._process_websocket_frame(WebSocket.TEXT, b'foo'),
(None, 'foo'))
self.assertEqual(ws._process_websocket_frame(WebSocket.BINARY, b'foo'),
(None, b'foo'))
self.assertRaises(OSError, ws._process_websocket_frame,
WebSocket.CLOSE, b'')
self.assertEqual(ws._process_websocket_frame(WebSocket.PING, b'foo'),
(WebSocket.PONG, b'foo'))
self.assertEqual(ws._process_websocket_frame(WebSocket.PONG, b'foo'),
(None, None))

View File

@@ -54,7 +54,7 @@ class TestMicrodotWSGI(unittest.TestCase):
def start_response(status, headers):
self.assertEqual(status, '200 OK')
expected_headers = [('Content-Length', '8'),
('Content-Type', 'text/plain'),
('Content-Type', 'text/plain; charset=UTF-8'),
('Set-Cookie', 'foo=foo'),
('Set-Cookie', 'bar=bar; HttpOnly')]
self.assertEqual(len(headers), len(expected_headers))

View File

@@ -16,7 +16,7 @@ def _run(coro):
class TestRequestAsync(unittest.TestCase):
def test_create_request(self):
fd = get_async_request_fd('GET', '/foo')
req = _run(Request.create('app', fd, 'addr'))
req = _run(Request.create('app', fd, 'writer', 'addr'))
self.assertEqual(req.app, 'app')
self.assertEqual(req.client_addr, 'addr')
self.assertEqual(req.method, 'GET')
@@ -37,7 +37,7 @@ class TestRequestAsync(unittest.TestCase):
'Content-Type': 'application/json',
'Cookie': 'foo=bar;abc=def',
'Content-Length': '3'}, body='aaa')
req = _run(Request.create('app', fd, 'addr'))
req = _run(Request.create('app', fd, 'writer', 'addr'))
self.assertEqual(req.headers, {
'Host': 'example.com:1234',
'Content-Type': 'application/json',
@@ -50,7 +50,7 @@ class TestRequestAsync(unittest.TestCase):
def test_args(self):
fd = get_async_request_fd('GET', '/?foo=bar&abc=def&x=%2f%%')
req = _run(Request.create('app', fd, 'addr'))
req = _run(Request.create('app', fd, 'writer', 'addr'))
self.assertEqual(req.query_string, 'foo=bar&abc=def&x=%2f%%')
self.assertEqual(req.args, MultiDict(
{'foo': 'bar', 'abc': 'def', 'x': '/%%'}))
@@ -58,26 +58,26 @@ class TestRequestAsync(unittest.TestCase):
def test_json(self):
fd = get_async_request_fd('GET', '/foo', headers={
'Content-Type': 'application/json'}, body='{"foo":"bar"}')
req = _run(Request.create('app', fd, 'addr'))
req = _run(Request.create('app', fd, 'writer', 'addr'))
json = req.json
self.assertEqual(json, {'foo': 'bar'})
self.assertTrue(req.json is json)
fd = get_async_request_fd('GET', '/foo', headers={
'Content-Type': 'application/json'}, body='[1, "2"]')
req = _run(Request.create('app', fd, 'addr'))
req = _run(Request.create('app', fd, 'writer', 'addr'))
self.assertEqual(req.json, [1, '2'])
fd = get_async_request_fd('GET', '/foo', headers={
'Content-Type': 'application/xml'}, body='[1, "2"]')
req = _run(Request.create('app', fd, 'addr'))
req = _run(Request.create('app', fd, 'writer', 'addr'))
self.assertIsNone(req.json)
def test_form(self):
fd = get_async_request_fd('GET', '/foo', headers={
'Content-Type': 'application/x-www-form-urlencoded'},
body='foo=bar&abc=def&x=%2f%%')
req = _run(Request.create('app', fd, 'addr'))
req = _run(Request.create('app', fd, 'writer', 'addr'))
form = req.form
self.assertEqual(form, MultiDict(
{'foo': 'bar', 'abc': 'def', 'x': '/%%'}))
@@ -86,7 +86,7 @@ class TestRequestAsync(unittest.TestCase):
fd = get_async_request_fd('GET', '/foo', headers={
'Content-Type': 'application/json'},
body='foo=bar&abc=def&x=%2f%%')
req = _run(Request.create('app', fd, 'addr'))
req = _run(Request.create('app', fd, 'writer', 'addr'))
self.assertIsNone(req.form)
def test_large_line(self):
@@ -97,7 +97,7 @@ class TestRequestAsync(unittest.TestCase):
'Content-Type': 'application/x-www-form-urlencoded'},
body='foo=bar&abc=def&x=y')
with self.assertRaises(ValueError):
_run(Request.create('app', fd, 'addr'))
_run(Request.create('app', fd, 'writer', 'addr'))
Request.max_readline = saved_max_readline
@@ -106,7 +106,7 @@ class TestRequestAsync(unittest.TestCase):
'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, 'writer', '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')
@@ -121,7 +121,7 @@ class TestRequestAsync(unittest.TestCase):
'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, 'writer', 'addr'))
self.assertEqual(req.body, b'')
data = _run(req.stream.read())
self.assertEqual(data, b'foo=bar&abc=def&x=y')

View File

@@ -20,7 +20,7 @@ class TestResponse(unittest.TestCase):
response = fd.getvalue()
self.assertIn(b'HTTP/1.0 200 OK\r\n', response)
self.assertIn(b'Content-Length: 3\r\n', response)
self.assertIn(b'Content-Type: text/plain\r\n', response)
self.assertIn(b'Content-Type: text/plain; charset=UTF-8\r\n', response)
self.assertTrue(response.endswith(b'\r\n\r\nfoo'))
def test_create_from_string_with_content_length(self):
@@ -33,7 +33,7 @@ class TestResponse(unittest.TestCase):
response = fd.getvalue()
self.assertIn(b'HTTP/1.0 200 OK\r\n', response)
self.assertIn(b'Content-Length: 2\r\n', response)
self.assertIn(b'Content-Type: text/plain\r\n', response)
self.assertIn(b'Content-Type: text/plain; charset=UTF-8\r\n', response)
self.assertTrue(response.endswith(b'\r\n\r\nfoo'))
def test_create_from_bytes(self):
@@ -46,7 +46,7 @@ class TestResponse(unittest.TestCase):
response = fd.getvalue()
self.assertIn(b'HTTP/1.0 200 OK\r\n', response)
self.assertIn(b'Content-Length: 3\r\n', response)
self.assertIn(b'Content-Type: text/plain\r\n', response)
self.assertIn(b'Content-Type: text/plain; charset=UTF-8\r\n', response)
self.assertTrue(response.endswith(b'\r\n\r\nfoo'))
def test_create_empty(self):
@@ -60,32 +60,36 @@ class TestResponse(unittest.TestCase):
self.assertIn(b'HTTP/1.0 200 OK\r\n', response)
self.assertIn(b'X-Foo: Bar\r\n', response)
self.assertIn(b'Content-Length: 0\r\n', response)
self.assertIn(b'Content-Type: text/plain\r\n', response)
self.assertIn(b'Content-Type: text/plain; charset=UTF-8\r\n', response)
self.assertTrue(response.endswith(b'\r\n\r\n'))
def test_create_json(self):
res = Response({'foo': 'bar'})
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers, {'Content-Type': 'application/json'})
self.assertEqual(res.headers,
{'Content-Type': 'application/json; charset=UTF-8'})
self.assertEqual(res.body, b'{"foo": "bar"}')
fd = io.BytesIO()
res.write(fd)
response = fd.getvalue()
self.assertIn(b'HTTP/1.0 200 OK\r\n', response)
self.assertIn(b'Content-Length: 14\r\n', response)
self.assertIn(b'Content-Type: application/json\r\n', response)
self.assertIn(b'Content-Type: application/json; charset=UTF-8\r\n',
response)
self.assertTrue(response.endswith(b'\r\n\r\n{"foo": "bar"}'))
res = Response([1, '2'])
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers, {'Content-Type': 'application/json'})
self.assertEqual(res.headers,
{'Content-Type': 'application/json; charset=UTF-8'})
self.assertEqual(res.body, b'[1, "2"]')
fd = io.BytesIO()
res.write(fd)
response = fd.getvalue()
self.assertIn(b'HTTP/1.0 200 OK\r\n', response)
self.assertIn(b'Content-Length: 8\r\n', response)
self.assertIn(b'Content-Type: application/json\r\n', response)
self.assertIn(b'Content-Type: application/json; charset=UTF-8\r\n',
response)
self.assertTrue(response.endswith(b'\r\n\r\n[1, "2"]'))
def test_create_from_none(self):
@@ -97,7 +101,7 @@ class TestResponse(unittest.TestCase):
response = fd.getvalue()
self.assertIn(b'HTTP/1.0 204 N/A\r\n', response)
self.assertIn(b'Content-Length: 0\r\n', response)
self.assertIn(b'Content-Type: text/plain\r\n', response)
self.assertIn(b'Content-Type: text/plain; charset=UTF-8\r\n', response)
self.assertTrue(response.endswith(b'\r\n\r\n'))
def test_create_from_other(self):
@@ -230,3 +234,21 @@ class TestResponse(unittest.TestCase):
response,
b'HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\nfoo\n')
Response.send_file_buffer_size = original_buffer_size
def test_default_content_type(self):
original_content_type = Response.default_content_type
res = Response('foo')
res.complete()
self.assertEqual(res.headers['Content-Type'],
'text/plain; charset=UTF-8')
Response.default_content_type = 'text/html'
res = Response('foo')
res.complete()
self.assertEqual(res.headers['Content-Type'],
'text/html; charset=UTF-8')
Response.default_content_type = 'text/html; charset=ISO-8859-1'
res = Response('foo')
res.complete()
self.assertEqual(res.headers['Content-Type'],
'text/html; charset=ISO-8859-1')
Response.default_content_type = original_content_type

View File

@@ -22,7 +22,8 @@ class TestResponseAsync(unittest.TestCase):
_run(res.write(fd))
self.assertIn(b'HTTP/1.0 200 OK\r\n', fd.response)
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain; charset=UTF-8\r\n',
fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nfoo'))
def test_create_from_string_with_content_length(self):
@@ -34,7 +35,8 @@ class TestResponseAsync(unittest.TestCase):
_run(res.write(fd))
self.assertIn(b'HTTP/1.0 200 OK\r\n', fd.response)
self.assertIn(b'Content-Length: 2\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain; charset=UTF-8\r\n',
fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nfoo'))
def test_create_from_bytes(self):
@@ -46,7 +48,8 @@ class TestResponseAsync(unittest.TestCase):
_run(res.write(fd))
self.assertIn(b'HTTP/1.0 200 OK\r\n', fd.response)
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain; charset=UTF-8\r\n',
fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nfoo'))
def test_create_empty(self):
@@ -59,30 +62,35 @@ class TestResponseAsync(unittest.TestCase):
self.assertIn(b'HTTP/1.0 200 OK\r\n', fd.response)
self.assertIn(b'X-Foo: Bar\r\n', fd.response)
self.assertIn(b'Content-Length: 0\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain; charset=UTF-8\r\n',
fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\n'))
def test_create_json(self):
res = Response({'foo': 'bar'})
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers, {'Content-Type': 'application/json'})
self.assertEqual(res.headers,
{'Content-Type': 'application/json; charset=UTF-8'})
self.assertEqual(res.body, b'{"foo": "bar"}')
fd = FakeStreamAsync()
_run(res.write(fd))
self.assertIn(b'HTTP/1.0 200 OK\r\n', fd.response)
self.assertIn(b'Content-Length: 14\r\n', fd.response)
self.assertIn(b'Content-Type: application/json\r\n', fd.response)
self.assertIn(b'Content-Type: application/json; charset=UTF-8\r\n',
fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\n{"foo": "bar"}'))
res = Response([1, '2'])
self.assertEqual(res.status_code, 200)
self.assertEqual(res.headers, {'Content-Type': 'application/json'})
self.assertEqual(res.headers,
{'Content-Type': 'application/json; charset=UTF-8'})
self.assertEqual(res.body, b'[1, "2"]')
fd = FakeStreamAsync()
_run(res.write(fd))
self.assertIn(b'HTTP/1.0 200 OK\r\n', fd.response)
self.assertIn(b'Content-Length: 8\r\n', fd.response)
self.assertIn(b'Content-Type: application/json\r\n', fd.response)
self.assertIn(b'Content-Type: application/json; charset=UTF-8\r\n',
fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\n[1, "2"]'))
def test_create_with_reason(self):

View File

@@ -41,7 +41,7 @@ class TestUTemplate(unittest.TestCase):
return render_template('hello.utemplate.txt', name='foo')
req = _run(RequestAsync.create(
app, get_async_request_fd('GET', '/'), 'addr'))
app, get_async_request_fd('GET', '/'), 'writer', 'addr'))
res = _run(app.dispatch_request(req))
self.assertEqual(res.status_code, 200)

View File

@@ -1,23 +1,21 @@
FROM ubuntu:latest
FROM ubuntu:20.04
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
apt-get install -y build-essential libffi-dev git pkg-config python python3 && \
apt-get install -y build-essential libffi-dev git pkg-config python3 && \
rm -rf /var/lib/apt/lists/* && \
git clone https://github.com/micropython/micropython.git && \
cd micropython && \
git checkout v1.15 && \
git submodule update --init && \
cd mpy-cross && \
make && \
cd .. && \
cd ports/unix && \
make axtls && \
make && \
make test && \
make install && \
apt-get purge --auto-remove -y build-essential libffi-dev git pkg-config python python3 && \
apt-get purge --auto-remove -y build-essential libffi-dev git pkg-config python3 && \
cd ../../.. && \
rm -rf micropython