Files
ws2/src/rom/nanoweb.py

192 lines
6.3 KiB
Python

import uasyncio as asyncio
import uerrno
__version__ = "1.0.0"
class HttpError(Exception):
pass
class Request:
url = ""
method = ""
headers = {}
route = ""
read = None
write = None
close = None
def __init__(self):
self.url = ""
self.method = ""
self.headers = {}
self.route = ""
self.read = None
self.write = None
self.close = None
async def write(request, data):
await request.write(data.encode("ISO-8859-1") if type(data) == str else data)
async def error(request, code, reason):
await request.write("HTTP/1.1 %s %s\r\n\r\n" % (code, reason))
await request.write(str(reason))
async def send_file(request, filename, segment=512, binary=True):
try:
with open(filename, "rb" if binary else "r") as f:
while True:
data = f.read(segment)
if not data:
break
await request.write(data)
except OSError as e:
if e.args[0] != uerrno.ENOENT:
raise
raise HttpError(request, 404, "File Not Found")
class Nanoweb:
extract_headers = ("Authorization", "Content-Length", "Content-Type")
headers = {}
routes = {}
assets_extensions = (".html", ".css", ".js", ".jpg", ".png", ".gif")
callback_request = None
callback_error = staticmethod(error)
STATIC_DIR = ""
INDEX_FILE = STATIC_DIR + "/index.html"
def __init__(self, port=80, address="0.0.0.0"):
self.port = port
self.address = address
def route(self, route):
"""Route decorator"""
def decorator(func):
self.routes[route] = func
return func
return decorator
async def generate_output(self, request, handler):
"""Generate output from handler
`handler` can be :
* dict representing the template context
* string, considered as a path to a file
* tuple where the first item is filename and the second
is the template context
* callable, the output of which is sent to the client
"""
while True:
if isinstance(handler, dict):
handler = (request.url, handler)
if isinstance(handler, str):
await write(request, "HTTP/1.1 200 OK\r\n")
await write(request, "Cache-Control: max-age=3600\r\n\r\n")
await send_file(request, handler)
elif isinstance(handler, tuple):
await write(request, "HTTP/1.1 200 OK\r\n\r\n")
filename, context = handler
context = context() if callable(context) else context
try:
with open(filename, "r") as f:
for l in f:
await write(request, l.format(**context))
except OSError as e:
if e.args[0] != uerrno.ENOENT:
raise
raise HttpError(request, 404, "File Not Found")
else:
handler = await handler(request)
if handler:
# handler can returns data that can be fed back
# to the input of the function
continue
break
async def handle(self, reader, writer):
items = await reader.readline()
items = items.decode("ascii").split()
if len(items) != 3:
return
request = Request()
request.read = reader.read
request.write = writer.awrite
request.close = writer.aclose
request.method, request.url, version = items
try:
try:
if version not in ("HTTP/1.0", "HTTP/1.1"):
raise HttpError(request, 505, "Version Not Supported")
while True:
items = await reader.readline()
items = items.decode("ascii").split(":", 1)
if len(items) == 2:
header, value = items
value = value.strip()
if header in self.extract_headers:
request.headers[header] = value
elif len(items) == 1:
break
if self.callback_request:
self.callback_request(request)
request_url = request.url.rstrip("/")
if request_url in self.routes:
# 1. If current url exists in routes
request.route = request_url
await self.generate_output(request, self.routes[request_url])
else:
# 2. Search url in routes with wildcard
for route, handler in self.routes.items():
if route == request_url or (
route[-1] == "*" and request_url.startswith(route[:-1])
):
request.route = route
await self.generate_output(request, handler)
break
else:
# 3. Try to load index file
if request_url in ("", "/"):
await self.generate_output(request, self.INDEX_FILE)
else:
# 4. Current url have an assets extension ?
for extension in self.assets_extensions:
if request_url.endswith(extension):
await self.generate_output(
request,
"%s%s" % (self.STATIC_DIR, request_url),
)
break
else:
raise HttpError(request, 404, "File Not Found")
except HttpError as e:
request, code, message = e.args
await self.callback_error(request, code, message)
except OSError as e:
# Skip ECONNRESET error (client abort request)
if e.args[0] != uerrno.ECONNRESET:
raise
finally:
await writer.aclose()
async def run(self):
return await asyncio.start_server(self.handle, self.address, self.port)