├── 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 | --------------------------------------------------------------------------------