Skip to content

Commit

Permalink
Tinyweb: Sync with upstream.
Browse files Browse the repository at this point in the history
Fixes #978.

Includes the following changes:

Adjust log.exc to log.exception
belyalov/tinyweb@7669f03
Logging module dropped support for exc. These adjustments use the exception method instead.

Co-authored-by: Stephen Jefferson <[email protected]>

force lowercase headers and force uppercase method
belyalov/tinyweb@b4393ac

Co-authored-by: eyJhb <[email protected]>

add compatibility for micropython above 1.19.0
belyalov/tinyweb@d067b98
* uasyncio is renamed to asyncio
* directly use core from asyncio

Co-authored-by: Fabian Clemenz <[email protected]>
  • Loading branch information
Gadgetoid and FabianClemenz committed Aug 13, 2024
1 parent c6384ce commit 1fffc9c
Show file tree
Hide file tree
Showing 2 changed files with 45 additions and 15 deletions.
Binary file modified micropython/examples/common/lib/tinyweb/server.mpy
Binary file not shown.
60 changes: 45 additions & 15 deletions micropython/examples/common/lib/tinyweb/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
(C) Konstantin Belyalov 2017-2018
"""
import logging
import uasyncio as asyncio
import uasyncio.core
import asyncio
import ujson as json
import gc
import uos as os
Expand All @@ -16,15 +15,18 @@

log = logging.getLogger('WEB')

type_gen = type((lambda: (yield))()) # noqa: E275
type_gen = type((lambda: (yield))())

# uasyncio v3 is shipped with MicroPython 1.13, and contains some subtle
# with v1.21.0 release all u-modules where renamend without the u prefix
# -> uasyncio no named asyncio
# asyncio v3 is shipped with MicroPython 1.13, and contains some subtle
# but breaking changes. See also https://github.com/peterhinch/micropython-async/blob/master/v3/README.md
IS_UASYNCIO_V3 = hasattr(asyncio, "__version__") and asyncio.__version__ >= (3,)
IS_ASYNCIO_V3 = hasattr(asyncio, "__version__") and asyncio.__version__ >= (3,)


def urldecode_plus(s):
"""Decode urlencoded string (including '+' char).
Returns decoded string
"""
s = s.replace('+', ' ')
Expand All @@ -42,6 +44,7 @@ def urldecode_plus(s):

def parse_query_string(s):
"""Parse urlencoded string into dict.
Returns dict
"""
res = {}
Expand Down Expand Up @@ -75,6 +78,7 @@ def __init__(self, _reader):
async def read_request_line(self):
"""Read and parse first line (AKA HTTP Request Line).
Function is generator.
Request line is something like:
GET /something/script?param1=val1 HTTP/1.1
"""
Expand All @@ -97,7 +101,9 @@ async def read_headers(self, save_headers=[]):
"""Read and parse HTTP headers until \r\n\r\n:
Optional argument 'save_headers' controls which headers to save.
This is done mostly to deal with memory constrains.
Function is generator.
HTTP headers could be like:
Host: google.com
Content-Type: blah
Expand All @@ -111,12 +117,13 @@ async def read_headers(self, save_headers=[]):
frags = line.split(b':', 1)
if len(frags) != 2:
raise HTTPException(400)
if frags[0] in save_headers:
if frags[0].lower() in save_headers:
self.headers[frags[0]] = frags[1].strip()

async def read_parse_form_data(self):
"""Read HTTP form data (payload), if any.
Function is generator.
Returns:
- dict of key / value pairs
- None in case of no form data present
Expand Down Expand Up @@ -163,6 +170,7 @@ async def _send_headers(self):
- HTTP request line
- HTTP headers following by \r\n.
This function is generator.
P.S.
Because of usually we have only a few HTTP headers (2-5) it doesn't make sense
to send them separately - sometimes it could increase latency.
Expand All @@ -181,8 +189,10 @@ async def _send_headers(self):
async def error(self, code, msg=None):
"""Generate HTTP error response
This function is generator.
Arguments:
code - HTTP response code
Example:
# Not enough permissions. Send HTTP 403 - Forbidden
await resp.error(403)
Expand All @@ -197,8 +207,10 @@ async def error(self, code, msg=None):
async def redirect(self, location, msg=None):
"""Generate HTTP redirect response to 'location'.
Basically it will generate HTTP 302 with 'Location' header
Arguments:
location - URL to redirect to
Example:
# Redirect to /something
await resp.redirect('/something')
Expand All @@ -213,9 +225,11 @@ async def redirect(self, location, msg=None):

def add_header(self, key, value):
"""Add HTTP response header
Arguments:
key - header name
value - header value
Example:
resp.add_header('Content-Encoding', 'gzip')
"""
Expand All @@ -232,6 +246,7 @@ def add_access_control_headers(self):
async def start_html(self):
"""Start response with HTML content type.
This function is generator.
Example:
await resp.start_html()
await resp.send('<html><h1>Hello, world!</h1></html>')
Expand All @@ -242,17 +257,21 @@ async def start_html(self):
async def send_file(self, filename, content_type=None, content_encoding=None, max_age=2592000, buf_size=128):
"""Send local file as HTTP response.
This function is generator.
Arguments:
filename - Name of file which exists in local filesystem
Keyword arguments:
content_type - Filetype. By default - None means auto-detect.
max_age - Cache control. How long browser can keep this file on disk.
By default - 30 days
Set to 0 - to disable caching.
Example 1: Default use case:
await resp.send_file('images/cat.jpg')
Example 2: Disable caching:
await resp.send_file('static/index.html', max_age=0)
Example 3: Override content type:
await resp.send_file('static/file.bin', content_type='application/octet-stream')
"""
Expand Down Expand Up @@ -331,7 +350,7 @@ async def restful_resource_handler(req, resp, param=None):
gc.collect()
await resp.send('0\r\n\r\n')
else:
if type(res) is tuple:
if type(res) == tuple:
resp.code = res[1]
res = res[0]
elif res is None:
Expand Down Expand Up @@ -457,22 +476,22 @@ async def _handler(self, reader, writer):
try:
await resp.error(500)
except Exception as e:
log.exc(e, "")
log.exception(f"Failed to send 500 error after OSError. Original error: {e}")
except HTTPException as e:
try:
await resp.error(e.code)
except Exception as e:
log.exc(e)
log.exception(f"Failed to send error after HTTPException. Original error: {e}")
except Exception as e:
# Unhandled expection in user's method
log.error(req.path.decode())
log.exc(e, "")
log.exception(f"Unhandled exception in user's method. Original error: {e}")
try:
await resp.error(500)
# Send exception info if desired
if self.debug:
sys.print_exception(e, resp.writer.s)
except Exception:
except Exception as e:
pass
finally:
await writer.aclose()
Expand All @@ -485,9 +504,11 @@ async def _handler(self, reader, writer):

def add_route(self, url, f, **kwargs):
"""Add URL to function mapping.
Arguments:
url - url to map function with
f - function to map
Keyword arguments:
methods - list of allowed methods. Defaults to ['GET', 'POST']
save_headers - contains list of HTTP headers to be saved. Case sensitive. Default - empty.
Expand All @@ -507,8 +528,8 @@ def add_route(self, url, f, **kwargs):
params.update(kwargs)
params['allowed_access_control_methods'] = ', '.join(params['methods'])
# Convert methods/headers to bytestring
params['methods'] = [x.encode() for x in params['methods']]
params['save_headers'] = [x.encode() for x in params['save_headers']]
params['methods'] = [x.encode().upper() for x in params['methods']]
params['save_headers'] = [x.encode().lower() for x in params['save_headers']]
# If URL has a parameter
if url.endswith('>'):
idx = url.rfind('<')
Expand All @@ -526,14 +547,18 @@ def add_route(self, url, f, **kwargs):

def add_resource(self, cls, url, **kwargs):
"""Map resource (RestAPI) to URL
Arguments:
cls - Resource class to map to
url - url to map to class
kwargs - User defined key args to pass to the handler.
Example:
class myres():
def get(self, data):
return {'hello': 'world'}
app.add_resource(myres, '/api/myres')
"""
methods = []
Expand All @@ -556,6 +581,7 @@ def get(self, data):

def catchall(self):
"""Decorator for catchall()
Example:
@app.catchall()
def catchall_handler(req, resp):
Expand All @@ -572,6 +598,7 @@ def _route(f):

def route(self, url, **kwargs):
"""Decorator for add_route()
Example:
@app.route('/')
def index(req, resp):
Expand All @@ -585,10 +612,12 @@ def _route(f):

def resource(self, url, method='GET', **kwargs):
"""Decorator for add_resource() method
Examples:
@app.resource('/users')
def users(data):
return {'a': 1}
@app.resource('/messages/<topic_id>')
async def index(data, topic_id):
yield '{'
Expand Down Expand Up @@ -617,8 +646,8 @@ async def _tcp_server(self, host, port, backlog):
sock.listen(backlog)
try:
while True:
if IS_UASYNCIO_V3:
yield uasyncio.core._io_queue.queue_read(sock)
if IS_ASYNCIO_V3:
yield asyncio.core._io_queue.queue_read(sock)
else:
yield asyncio.IORead(sock)
csock, caddr = sock.accept()
Expand All @@ -645,6 +674,7 @@ async def _tcp_server(self, host, port, backlog):

def run(self, host="127.0.0.1", port=8081, loop_forever=True):
"""Run Web Server. By default it runs forever.
Keyword arguments:
host - host to listen on. By default - localhost (127.0.0.1)
port - port to listen on. By default - 8081
Expand Down

0 comments on commit 1fffc9c

Please sign in to comment.