├── LICENSE
├── README.md
├── examples
├── hello-world
│ └── main.py
├── sse
│ └── main.py
├── streaming
│ └── main.py
└── websocket
│ └── main.py
└── web.py
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 davy wybiral
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # micropython-aioweb
2 | A very minimal asyncio web framework for MicroPython. Doesn't come with all the bells and whistles you might want out of a serious web framework but the goal is just to make asyncio HTTP applications in MicroPython as simple and efficient as possible.
3 |
4 | ## Current features
5 | * minimal overhead in terms of code size or memory use
6 | * easy integration into existing asyncio projects by running as a normal task alongside others
7 | * basic endpoint/method based routing similar to flask (currently doesn't do any pattern matching)
8 | * parses http request line, headers, and query strings
9 | * supports WebSockets!
10 | * supports Server-Sent Events!
11 |
12 | ## Examples
13 | ### Basic "Hello world!"
14 | ```python
15 | import web
16 | import uasyncio as asyncio
17 |
18 | app = web.App(host='0.0.0.0', port=80)
19 |
20 | # root route handler
21 | @app.route('/')
22 | async def handler(r, w):
23 | # write http headers
24 | w.write(b'HTTP/1.0 200 OK\r\n')
25 | w.write(b'Content-Type: text/html; charset=utf-8\r\n')
26 | w.write(b'\r\n')
27 | # write page body
28 | w.write(b'Hello world!')
29 | # drain stream buffer
30 | await w.drain()
31 |
32 | # Start event loop and create server task
33 | loop = asyncio.get_event_loop()
34 | loop.create_task(app.serve())
35 | loop.run_forever()
36 | ```
37 | ### POST request handler
38 | ```python
39 | @app.route('/', methods=['POST'])
40 | async def handler(r, w):
41 | body = await r.read(1024)
42 | form = web.parse_qs(body.decode())
43 | name = form.get('name', 'world')
44 | # write http headers
45 | w.write(b'HTTP/1.0 200 OK\r\n')
46 | w.write(b'Content-Type: text/html; charset=utf-8\r\n')
47 | w.write(b'\r\n')
48 | # write page body
49 | w.write(b'Hello {}!'.format(name))
50 | # drain stream buffer
51 | await w.drain()
52 | ```
53 | ### WebSocket handler
54 | ```python
55 | # /ws WebSocket route handler
56 | @app.route('/ws')
57 | async def ws_handler(r, w):
58 | # upgrade connection to WebSocket
59 | ws = await WebSocket.upgrade(r, w)
60 | while True:
61 | evt = await ws.recv()
62 | if evt is None or evt['type'] == 'close':
63 | # handle closed stream/close event
64 | break
65 | elif evt['type'] == 'text':
66 | # print received messages and echo them
67 | print('Received:', evt['data'])
68 | await ws.send(evt['data'])
69 | ```
70 | ### SSE (Server-Sent Events) handler
71 | ```python
72 | # /events EventSource route handler
73 | @app.route('/events')
74 | async def ws_handler(r, w):
75 | # upgrade connection to text/event-stream
76 | sse = await web.EventSource.upgrade(r, w)
77 | count = 0
78 | while True:
79 | count += 1
80 | try:
81 | await sse.send('Hello world #{}'.format(count))
82 | except:
83 | break
84 | await asyncio.sleep(1)
85 | ```
86 |
--------------------------------------------------------------------------------
/examples/hello-world/main.py:
--------------------------------------------------------------------------------
1 | import network
2 | import web
3 | import uasyncio as asyncio
4 |
5 | # access point credentials
6 | AP_SSID = 'Hello AP'
7 | AP_PASSWORD = 'donthackmebro'
8 | AP_AUTHMODE = network.AUTH_WPA_WPA2_PSK
9 |
10 | app = web.App(host='0.0.0.0', port=80)
11 |
12 | # root route handler
13 | @app.route('/')
14 | async def handler(r, w):
15 | w.write(b'HTTP/1.0 200 OK\r\n')
16 | w.write(b'Content-Type: text/html; charset=utf-8\r\n')
17 | w.write(b'\r\n')
18 | w.write(b'Hello world!')
19 | await w.drain()
20 |
21 | # Create WiFi access point
22 | wifi = network.WLAN(network.AP_IF)
23 | wifi.active(True)
24 | wifi.config(essid=AP_SSID, password=AP_PASSWORD, authmode=AP_AUTHMODE)
25 | while wifi.active() == False:
26 | pass
27 | print(wifi.ifconfig())
28 |
29 | # Start event loop and create server task
30 | loop = asyncio.get_event_loop()
31 | loop.create_task(app.serve())
32 | loop.run_forever()
33 |
--------------------------------------------------------------------------------
/examples/sse/main.py:
--------------------------------------------------------------------------------
1 | import network
2 | import web
3 | import uasyncio as asyncio
4 |
5 | # access point credentials
6 | AP_SSID = 'SSE AP'
7 | AP_PASSWORD = 'donthackmebro'
8 | AP_AUTHMODE = network.AUTH_WPA_WPA2_PSK
9 |
10 | app = web.App(host='0.0.0.0', port=80)
11 |
12 | # root route handler
13 | @app.route('/')
14 | async def index_handler(r, w):
15 | w.write(b'HTTP/1.0 200 OK\r\n')
16 | w.write(b'Content-Type: text/html; charset=utf-8\r\n')
17 | w.write(b'\r\n')
18 | w.write(b'''
29 | ''')
30 | await w.drain()
31 |
32 | # /events EventSource handler
33 | @app.route('/events')
34 | async def events_handler(r, w):
35 | # upgrade connection to EventSource (Server-Sent Events)
36 | sse = await web.EventSource.upgrade(r, w)
37 | count = 0
38 | while True:
39 | count += 1
40 | try:
41 | await sse.send('Hello world #{}'.format(count))
42 | except:
43 | break
44 | await asyncio.sleep(1)
45 |
46 | # Create WiFi access point
47 | wifi = network.WLAN(network.AP_IF)
48 | wifi.active(True)
49 | wifi.config(essid=AP_SSID, password=AP_PASSWORD, authmode=AP_AUTHMODE)
50 | while wifi.active() == False:
51 | pass
52 | print(wifi.ifconfig())
53 |
54 | # Start event loop and create server task
55 | loop = asyncio.get_event_loop()
56 | loop.create_task(app.serve())
57 | loop.run_forever()
58 |
--------------------------------------------------------------------------------
/examples/streaming/main.py:
--------------------------------------------------------------------------------
1 | import network
2 | import web
3 | import uasyncio as asyncio
4 |
5 | # access point credentials
6 | AP_SSID = 'Streaming AP'
7 | AP_PASSWORD = 'donthackmebro'
8 | AP_AUTHMODE = network.AUTH_WPA_WPA2_PSK
9 |
10 | app = web.App(host='0.0.0.0', port=80)
11 |
12 | # root route handler
13 | @app.route('/')
14 | async def handler(r, w):
15 | w.write(b'HTTP/1.0 200 OK\r\n')
16 | w.write(b'Content-Type: text/html; charset=utf-8\r\n')
17 | w.write(b'\r\n')
18 | await w.drain()
19 | count = 0
20 | while True:
21 | count += 1
22 | w.write(b'Hello world #{}!
'.format(count))
23 | try:
24 | await w.drain()
25 | except:
26 | break
27 | await asyncio.sleep(1)
28 |
29 | # Create WiFi access point
30 | wifi = network.WLAN(network.AP_IF)
31 | wifi.active(True)
32 | wifi.config(essid=AP_SSID, password=AP_PASSWORD, authmode=AP_AUTHMODE)
33 | while wifi.active() == False:
34 | pass
35 | print(wifi.ifconfig())
36 |
37 | # Start event loop and create server task
38 | loop = asyncio.get_event_loop()
39 | loop.create_task(app.serve())
40 | loop.run_forever()
41 |
--------------------------------------------------------------------------------
/examples/websocket/main.py:
--------------------------------------------------------------------------------
1 | import network
2 | import web
3 | import uasyncio as asyncio
4 |
5 | # access point credentials
6 | AP_SSID = 'WebSocket AP'
7 | AP_PASSWORD = 'donthackmebro'
8 | AP_AUTHMODE = network.AUTH_WPA_WPA2_PSK
9 |
10 | app = web.App(host='0.0.0.0', port=80)
11 |
12 | # root route handler
13 | @app.route('/')
14 | async def index_handler(r, w):
15 | w.write(b'HTTP/1.0 200 OK\r\n')
16 | w.write(b'Content-Type: text/html; charset=utf-8\r\n')
17 | w.write(b'\r\n')
18 | w.write(b'''
36 |
37 |
38 |
39 | ''')
40 | await w.drain()
41 |
42 | # Store current WebSocket clients
43 | WS_CLIENTS = set()
44 |
45 | # /ws WebSocket route handler
46 | @app.route('/ws')
47 | async def ws_handler(r, w):
48 | global WS_CLIENTS
49 | # upgrade connection to WebSocket
50 | ws = await web.WebSocket.upgrade(r, w)
51 | # add current client to set
52 | WS_CLIENTS.add(ws)
53 | while True:
54 | # handle ws events
55 | evt = await ws.recv()
56 | if evt is None or evt['type'] == 'close':
57 | break
58 | elif evt['type'] == 'text':
59 | # echo received message to all clients
60 | for ws_client in WS_CLIENTS:
61 | try:
62 | await ws_client.send(evt['data'])
63 | except:
64 | continue
65 | # remove current client from set
66 | WS_CLIENTS.discard(ws)
67 |
68 | # Create WiFi access point
69 | wifi = network.WLAN(network.AP_IF)
70 | wifi.active(True)
71 | wifi.config(essid=AP_SSID, password=AP_PASSWORD, authmode=AP_AUTHMODE)
72 | while wifi.active() == False:
73 | pass
74 | print(wifi.ifconfig())
75 |
76 | # Start event loop and create server task
77 | loop = asyncio.get_event_loop()
78 | loop.create_task(app.serve())
79 | loop.run_forever()
80 |
--------------------------------------------------------------------------------
/web.py:
--------------------------------------------------------------------------------
1 | import uasyncio as asyncio
2 | from hashlib import sha1
3 | from binascii import b2a_base64
4 | import struct
5 |
6 | def unquote_plus(s):
7 | out = []
8 | i = 0
9 | n = len(s)
10 | while i < n:
11 | c = s[i]
12 | i += 1
13 | if c == '+':
14 | out.append(' ')
15 | elif c == '%':
16 | out.append(chr(int(s[i:i + 2], 16)))
17 | i += 2
18 | else:
19 | out.append(c)
20 | return ''.join(out)
21 |
22 | def parse_qs(s):
23 | out = {}
24 | for x in s.split('&'):
25 | kv = x.split('=', 1)
26 | key = unquote_plus(kv[0])
27 | kv[0] = key
28 | if len(kv) == 1:
29 | val = True
30 | kv.append(val)
31 | else:
32 | val = unquote_plus(kv[1])
33 | kv[1] = val
34 | tmp = out.get(key, None)
35 | if tmp is None:
36 | out[key] = val
37 | else:
38 | if isinstance(tmp, list):
39 | tmp.append(val)
40 | else:
41 | out[key] = [tmp, val]
42 | return out
43 |
44 | async def _parse_request(r, w):
45 | line = await r.readline()
46 | if not line:
47 | raise ValueError
48 | parts = line.decode().split()
49 | if len(parts) < 3:
50 | raise ValueError
51 | r.method = parts[0]
52 | r.path = parts[1]
53 | parts = r.path.split('?', 1)
54 | if len(parts) < 2:
55 | r.query = None
56 | else:
57 | r.path = parts[0]
58 | r.query = parts[1]
59 | r.headers = await _parse_headers(r)
60 |
61 | async def _parse_headers(r):
62 | headers = {}
63 | while True:
64 | line = await r.readline()
65 | if not line:
66 | break
67 | line = line.decode()
68 | if line == '\r\n':
69 | break
70 | key, value = line.split(':', 1)
71 | headers[key.lower()] = value.strip()
72 | return headers
73 |
74 |
75 | class App:
76 |
77 | def __init__(self, host='0.0.0.0', port=80):
78 | self.host = host
79 | self.port = port
80 | self.handlers = []
81 |
82 | def route(self, path, methods=['GET']):
83 | def wrapper(handler):
84 | self.handlers.append((path, methods, handler))
85 | return handler
86 | return wrapper
87 |
88 | async def _dispatch(self, r, w):
89 | try:
90 | await _parse_request(r, w)
91 | except:
92 | await w.wait_closed()
93 | return
94 | for path, methods, handler in self.handlers:
95 | if r.path != path:
96 | continue
97 | if r.method not in methods:
98 | continue
99 | await handler(r, w)
100 | await w.wait_closed()
101 | return
102 | await w.awrite(b'HTTP/1.0 404 Not Found\r\n\r\nNot Found')
103 | await w.wait_closed()
104 |
105 | async def serve(self):
106 | await asyncio.start_server(self._dispatch, self.host, self.port)
107 |
108 |
109 | class WebSocket:
110 |
111 | HANDSHAKE_KEY = b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
112 |
113 | OP_TYPES = {
114 | 0x0: 'cont',
115 | 0x1: 'text',
116 | 0x2: 'bytes',
117 | 0x8: 'close',
118 | 0x9: 'ping',
119 | 0xa: 'pong',
120 | }
121 |
122 | @classmethod
123 | async def upgrade(cls, r, w):
124 | key = r.headers['sec-websocket-key'].encode()
125 | key += WebSocket.HANDSHAKE_KEY
126 | x = b2a_base64(sha1(key).digest()).strip()
127 | w.write(b'HTTP/1.1 101 Switching Protocols\r\n')
128 | w.write(b'Upgrade: websocket\r\n')
129 | w.write(b'Connection: Upgrade\r\n')
130 | w.write(b'Sec-WebSocket-Accept: ' + x + b'\r\n')
131 | w.write(b'\r\n')
132 | await w.drain()
133 | return cls(r, w)
134 |
135 | def __init__(self, r, w):
136 | self.r = r
137 | self.w = w
138 |
139 | async def recv(self):
140 | r = self.r
141 | x = await r.read(2)
142 | if not x or len(x) < 2:
143 | return None
144 | out = {}
145 | op, n = struct.unpack('!BB', x)
146 | out['fin'] = bool(op & (1 << 7))
147 | op = op & 0x0f
148 | if op not in WebSocket.OP_TYPES:
149 | raise None
150 | out['type'] = WebSocket.OP_TYPES[op]
151 | masked = bool(n & (1 << 7))
152 | n = n & 0x7f
153 | if n == 126:
154 | n, = struct.unpack('!H', await r.read(2))
155 | elif n == 127:
156 | n, = struct.unpack('!Q', await r.read(8))
157 | if masked:
158 | mask = await r.read(4)
159 | data = await r.read(n)
160 | if masked:
161 | data = bytearray(data)
162 | for i in range(len(data)):
163 | data[i] ^= mask[i % 4]
164 | data = bytes(data)
165 | if out['type'] == 'text':
166 | data = data.decode()
167 | out['data'] = data
168 | return out
169 |
170 | async def send(self, msg):
171 | if isinstance(msg, str):
172 | await self._send_op(0x1, msg.encode())
173 | elif isinstance(msg, bytes):
174 | await self._send_op(0x2, msg)
175 |
176 | async def _send_op(self, opcode, payload):
177 | w = self.w
178 | w.write(bytes([0x80 | opcode]))
179 | n = len(payload)
180 | if n < 126:
181 | w.write(bytes([n]))
182 | elif n < 65536:
183 | w.write(struct.pack('!BH', 126, n))
184 | else:
185 | w.write(struct.pack('!BQ', 127, n))
186 | w.write(payload)
187 | await w.drain()
188 |
189 |
190 | class EventSource:
191 |
192 | @classmethod
193 | async def upgrade(cls, r, w):
194 | w.write(b'HTTP/1.0 200 OK\r\n')
195 | w.write(b'Content-Type: text/event-stream\r\n')
196 | w.write(b'Cache-Control: no-cache\r\n')
197 | w.write(b'Connection: keep-alive\r\n')
198 | w.write(b'Access-Control-Allow-Origin: *\r\n')
199 | w.write(b'\r\n')
200 | await w.drain()
201 | return cls(r, w)
202 |
203 | def __init__(self, r, w):
204 | self.r = r
205 | self.w = w
206 |
207 | async def send(self, msg, id=None, event=None):
208 | w = self.w
209 | if id is not None:
210 | w.write(b'id: {}\r\n'.format(id))
211 | if event is not None:
212 | w.write(b'event: {}\r\n'.format(event))
213 | w.write(b'data: {}\r\n'.format(msg))
214 | w.write(b'\r\n')
215 | await w.drain()
216 |
--------------------------------------------------------------------------------