refactor and rewrite for asyncio
This commit is contained in:
+157
-198
@@ -1,20 +1,23 @@
|
||||
"""HTTP server and handler scaffolds (thin wrappers around http.server)."""
|
||||
from http import server
|
||||
"""HTTP server implementation using aiohttp and jinja2."""
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
import urllib.parse
|
||||
|
||||
from urllib3 import request
|
||||
import os
|
||||
import logging
|
||||
from aiohttp import web
|
||||
from fastapi.templating import Jinja2Templates
|
||||
import jinja2
|
||||
|
||||
class HttpServer(server.ThreadingHTTPServer):
|
||||
allow_reuse_address = True
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def threaded(self):
|
||||
pass
|
||||
def _render_template(html_str: str, **context) -> str:
|
||||
tmpl = jinja2.Template(html_str)
|
||||
return tmpl.render(**context)
|
||||
|
||||
|
||||
def make_handler_class(
|
||||
async def start(
|
||||
host: str,
|
||||
port: int,
|
||||
config,
|
||||
hbdclass,
|
||||
msgs_getter,
|
||||
@@ -28,209 +31,165 @@ def make_handler_class(
|
||||
get_now=None,
|
||||
VER="",
|
||||
):
|
||||
"""Return a BaseHTTPRequestHandler subclass bound to runtime objects.
|
||||
"""Start an aiohttp web server and block until cancelled.
|
||||
|
||||
`msgs_getter` should be a callable that returns a list-like of messages.
|
||||
This function is intended to be awaited inside the main asyncio event loop.
|
||||
"""
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
get_now = get_now or (lambda: time.time())
|
||||
|
||||
class CustomHandler(server.BaseHTTPRequestHandler):
|
||||
async def index(request):
|
||||
res = []
|
||||
res.append('<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">')
|
||||
res.append("<html>")
|
||||
res.append("<head>")
|
||||
res.append(f"<title>Heartbeat</title>")
|
||||
if tcss:
|
||||
res.append(tcss)
|
||||
res.append("</head>")
|
||||
res.append('<body BGCOLOR = "#FFFFFF" LINK = "#008000" VLINK = "#008000">')
|
||||
res.append(f"<H2>Heartbeat status {VER}</h2>")
|
||||
res += hbdclass.ubHost.buildhosttable()
|
||||
res += hbdclass.ubHost.buildmsgtable(msgs_getter())
|
||||
res.append(
|
||||
"<p> %s (%s)</p>" % (time.strftime("%H:%M:%S", time.localtime(get_now())), config.get("tz", "CET-1CDT"))
|
||||
)
|
||||
res.append("</body></html>")
|
||||
body = "\n".join(res)
|
||||
return web.Response(text=body, content_type="text/html")
|
||||
|
||||
server_version = f"HeartbeatHTTP/{VER}"
|
||||
async def api_hosts(request):
|
||||
lst = [hbdclass.Host.hosts[h].jsons() for h in hbdclass.Host.hosts]
|
||||
return web.json_response(json.loads("[" + ",".join(lst) + "]"))
|
||||
|
||||
def version_string(self):
|
||||
return self.server_version
|
||||
async def api_messages(request):
|
||||
lst = msgs_getter()[-30:]
|
||||
return web.json_response(lst)
|
||||
|
||||
def handle(self):
|
||||
async def cmd(request):
|
||||
qa = request.rel_url.query
|
||||
uname = qa.get("h")
|
||||
ucmd = qa.get("c")
|
||||
if not ucmd or not uname:
|
||||
return web.Response(status=400, text="need h= and c= arguments")
|
||||
if uname not in hbdclass.Host.hosts:
|
||||
return web.Response(status=400, text=f"h={uname} not found")
|
||||
hbdclass.Host.hosts[uname].cmds.append(("CMD", {"cmd": urllib.parse.unquote(ucmd)}))
|
||||
return web.Response(text=f"cmd {uname} queued")
|
||||
|
||||
async def drop(request):
|
||||
qa = request.rel_url.query
|
||||
uname = qa.get("h")
|
||||
if not uname:
|
||||
return web.Response(status=400, text="need h= argument")
|
||||
if uname not in hbdclass.Host.hosts:
|
||||
return web.Response(status=400, text=f"h={uname} not found")
|
||||
if log:
|
||||
log(uname, "dropped")
|
||||
del hbdclass.Host.hosts[uname]
|
||||
return web.Response(text="Done")
|
||||
|
||||
async def register(request):
|
||||
qa = request.rel_url.query
|
||||
uname = qa.get("h")
|
||||
if not uname:
|
||||
return web.Response(status=400, text="need h= argument")
|
||||
if uname not in hbdclass.Host.hosts:
|
||||
return web.Response(status=400, text=f"h={uname} not found")
|
||||
ll = hbdclass.Host.hosts[uname].registerDns()
|
||||
if log:
|
||||
log(uname, ll)
|
||||
return web.Response(text=str(ll))
|
||||
|
||||
async def update(request):
|
||||
qa = request.rel_url.query
|
||||
uname = urllib.parse.unquote(qa.get("h", ""))
|
||||
ucode = qa.get("c")
|
||||
if not ucode or not uname:
|
||||
return web.Response(status=400, text="need h= and c= arguments")
|
||||
if uname != "All" and uname not in hbdclass.Host.hosts:
|
||||
return web.Response(status=400, text=f"h={uname} not found")
|
||||
if uname != "All":
|
||||
names = [uname]
|
||||
else:
|
||||
names = [n for n in hbdclass.Host.hosts if hbdclass.Host.hosts[n].cver >= 2]
|
||||
out = []
|
||||
for n in names:
|
||||
err = None
|
||||
try:
|
||||
return server.BaseHTTPRequestHandler.handle(self)
|
||||
r = {"csum": None, "code": ucode}
|
||||
hbdclass.Host.hosts[n].cmds.append(("UPD", r))
|
||||
except Exception as e:
|
||||
self.log_error("Request went away: %r", e)
|
||||
self.close_connection = 1
|
||||
return
|
||||
err = str(e)
|
||||
out.append(f"update started for {n}: {err if err else 'OK'}")
|
||||
return web.Response(text="\n".join(out))
|
||||
|
||||
def do_HEAD(self):
|
||||
self.setheaders(200)
|
||||
async def restart(request):
|
||||
# signal main application to perform restart if needed
|
||||
# not implemented here - return OK
|
||||
if log:
|
||||
log(None, "restart request")
|
||||
return web.Response(text="restart request")
|
||||
|
||||
def setheaders(self, code, headerdict={}):
|
||||
self.send_response(code)
|
||||
self.send_header(
|
||||
"Last-Modified",
|
||||
time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(get_now())),
|
||||
)
|
||||
for h in headerdict:
|
||||
self.send_header(h, headerdict[h])
|
||||
self.end_headers()
|
||||
async def live(request):
|
||||
# render template from templates/live.html using Jinja2
|
||||
env = jinja2.Environment(loader=jinja2.FileSystemLoader(config.get("templates_dir", "templates")))
|
||||
host = config.get("hb_host", "localhost")
|
||||
extra_scripts = config.get("http_extra_scripts", "")
|
||||
heartbeat_ws_url = f"ws://{host}:{config.get('ws_port', 50005)}/hbd"
|
||||
tmpl = env.get_template("live.html")
|
||||
body = tmpl.render(
|
||||
title="Heartbeat",
|
||||
header="Heartbeat",
|
||||
request=request,
|
||||
heartbeat_ws_url=heartbeat_ws_url,
|
||||
extra_scripts=extra_scripts,
|
||||
hosts=[hbdclass.Host.hosts[h].stateinfo() for h in sorted(hbdclass.Host.hosts)],
|
||||
messages=msgs_getter()[-30:],
|
||||
)
|
||||
return web.Response(text=body, content_type="text/html")
|
||||
|
||||
def buildhead(self, title="Heartbeat", refresh=None, extras=None):
|
||||
res = []
|
||||
res.append('<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">')
|
||||
res.append("<html>")
|
||||
res.append("<head>")
|
||||
res.append("<title>%s</title>" % (title))
|
||||
if refresh:
|
||||
res.append("<meta http-equiv = Refresh content = %d>\n" % refresh)
|
||||
if extras:
|
||||
res.append(extras)
|
||||
res.append("</head>")
|
||||
res.append('<body BGCOLOR = "#FFFFFF" LINK = "#008000" VLINK = "#008000">')
|
||||
return res
|
||||
async def static(request):
|
||||
"""Serve files from the package static directory.
|
||||
|
||||
def buildpage(self):
|
||||
res = self.buildhead(refresh=60, extras=tcss)
|
||||
res.append("<H2>Heartbeat status %s</h2>" % VER)
|
||||
res += hbdclass.ubHost.buildhosttable()
|
||||
res += hbdclass.ubHost.buildmsgtable(msgs_getter())
|
||||
res.append(
|
||||
"<p> %s (%s)</p>" % (time.strftime("%H:%M:%S", time.localtime(get_now())), config.get("tz", "CET-1CDT"))
|
||||
)
|
||||
res.append("</body></html>")
|
||||
return res
|
||||
URL form: /static/<path>
|
||||
"""
|
||||
p = request.match_info.get("path", "")
|
||||
base = os.path.abspath(os.path.join(os.path.dirname(__file__), "static"))
|
||||
# normalize and prevent directory traversal
|
||||
target = os.path.abspath(os.path.normpath(os.path.join(base, p)))
|
||||
if not target.startswith(base + os.sep) and target != base:
|
||||
return web.Response(status=403, text="Forbidden")
|
||||
if not os.path.exists(target) or not os.path.isfile(target):
|
||||
return web.Response(status=404, text="Not Found")
|
||||
logger.info("serving static file: %s", target)
|
||||
return web.FileResponse(path=target)
|
||||
|
||||
def builderror(self, code, cause, lcause):
|
||||
res = []
|
||||
res.append('<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">')
|
||||
res.append("<html><head>")
|
||||
res.append("<title>%s %s</title>" % (code, cause))
|
||||
res.append("</head><body>")
|
||||
res.append("<h1>%s</h1>" % (cause))
|
||||
res.append("<p>%s</p>" % lcause)
|
||||
res.append("<hr>")
|
||||
res.append(
|
||||
"<address>hbd (Unix) Server at %s:%s</address>" % (config.get("hbd_host"), config.get("hbd_port"))
|
||||
)
|
||||
res.append("</body></html>")
|
||||
return code, res
|
||||
app = web.Application()
|
||||
app.add_routes(
|
||||
[
|
||||
web.get("/", index),
|
||||
web.get("/api/0/hosts", api_hosts),
|
||||
web.get("/api/0/messages", api_messages),
|
||||
web.get("/c", cmd),
|
||||
web.get("/d", drop),
|
||||
web.get("/n", register),
|
||||
web.get("/u", update),
|
||||
web.get("/r", restart),
|
||||
web.get("/live", live),
|
||||
web.get("/static/{path:.*}", static),
|
||||
]
|
||||
)
|
||||
|
||||
def do_GET(self):
|
||||
xsig = 0
|
||||
rqAcceptEncoding = self.headers.get("Accept-encoding", {})
|
||||
headerdict = {"Content-Type": "text/html; charset = ISO-8859-1"}
|
||||
qr = urllib.parse.urlparse(self.path)
|
||||
qa = urllib.parse.parse_qs(qr.query)
|
||||
runner = web.AppRunner(app)
|
||||
await runner.setup()
|
||||
site = web.TCPSite(runner, host, port)
|
||||
await site.start()
|
||||
|
||||
if qr.path == "/":
|
||||
res = self.buildpage()
|
||||
if verbose:
|
||||
print(f"HTTP server started on {host}:{port}")
|
||||
|
||||
elif qr.path == "/c": # command on host /c?h=melschserver&c=sudo%20ls
|
||||
uname = qa.get("h", [None])[0]
|
||||
ucmd = qa.get("c", [None])[0]
|
||||
if not ucmd or not uname:
|
||||
code, res = self.builderror(400, "Argument error", "need h= and c= arguments")
|
||||
elif uname not in hbdclass.Host.hosts:
|
||||
code, res = self.builderror(400, "Data error", "h=%s not found" % uname)
|
||||
else:
|
||||
hbdclass.Host.hosts[uname].cmds.append(("CMD", {"cmd": urllib.parse.unquote(ucmd)}))
|
||||
res = self.buildhead()
|
||||
res.append("cmd %s queued for host %s" % (uname, ucmd))
|
||||
try:
|
||||
await asyncio.Future()
|
||||
finally:
|
||||
await runner.cleanup()
|
||||
|
||||
elif qr.path == "/d": # drop host /d?h=melschserver
|
||||
uname = qa.get("h", [None])[0]
|
||||
if not uname:
|
||||
code, res = self.builderror(400, "Argument error", "need h= argument")
|
||||
if uname not in hbdclass.Host.hosts:
|
||||
code, res = self.builderror(400, "Data error", "h=%s not found" % uname)
|
||||
else:
|
||||
if log:
|
||||
log(uname, "dropped")
|
||||
del hbdclass.Host.hosts[uname]
|
||||
res = self.buildhead()
|
||||
res.append("Done")
|
||||
|
||||
elif qr.path == "/n": # register name
|
||||
uname = qa.get("h", [None])[0]
|
||||
if not uname:
|
||||
code, res = self.builderror(400, "Argument error", "need h= argument")
|
||||
if uname not in hbdclass.Host.hosts:
|
||||
code, res = self.builderror(400, "Data error", "h=%s not found" % uname)
|
||||
else:
|
||||
ll = hbdclass.Host.hosts[uname].registerDns()
|
||||
res = self.buildhead()
|
||||
res.append(ll)
|
||||
if log:
|
||||
log(uname, ll)
|
||||
|
||||
elif qr.path == "/u": # update
|
||||
uname = urllib.parse.unquote(qa.get("h", [None])[0])
|
||||
ucode = qa.get("c", [None])[0]
|
||||
if not ucode or not uname:
|
||||
code, res = self.builderror(400, "Argument error", "need h= and c= arguments")
|
||||
elif uname != "All" and uname not in hbdclass.Host.hosts:
|
||||
code, res = self.builderror(400, "Data error", "h=%s not found" % uname)
|
||||
else:
|
||||
res = self.buildhead()
|
||||
if uname != "All":
|
||||
names = [uname]
|
||||
else:
|
||||
names = []
|
||||
for n in hbdclass.Host.hosts:
|
||||
if hbdclass.Host.hosts[n].cver >= 2: # earliest version that supports update
|
||||
names.append(n)
|
||||
for n in names:
|
||||
err = None
|
||||
try:
|
||||
from hbd import proto
|
||||
# read code from a file name, fallback to sending ucode as data
|
||||
err = None
|
||||
# attempt to send update command to host
|
||||
r = {"csum": None, "code": ucode}
|
||||
hbdclass.Host.hosts[n].cmds.append(("UPD", r))
|
||||
except Exception as e:
|
||||
err = str(e)
|
||||
res.append("update started for %s: %s<br>" % (n, err if err else "OK"))
|
||||
res.append("Done")
|
||||
|
||||
elif qr.path == "/api/0/hosts": # api access to host table
|
||||
headerdict = {"Content-Type": "application/json; charset=utf-8"}
|
||||
lst = []
|
||||
for h in hbdclass.Host.hosts:
|
||||
lst.append(hbdclass.Host.hosts[h].jsons())
|
||||
res = ["[" + ",".join(lst) + "]"]
|
||||
|
||||
elif qr.path == "/api/0/messages": # api access to host table
|
||||
headerdict = {"Content-Type": "application/json; charset=utf-8"}
|
||||
lst = msgs_getter()[-30:]
|
||||
res = [json.dumps(lst)]
|
||||
|
||||
elif qr.path == "/r": # restart
|
||||
res = self.buildhead()
|
||||
res.append("restart request")
|
||||
xsig = 1 # signal.SIGHUP will be handled by application
|
||||
if log:
|
||||
log(None, "restart request")
|
||||
elif qr.path == "/live": # show live view with websockets
|
||||
host = config.get("hb_host", "localhost")
|
||||
extra_scripts = '' # '<script src="/static/js/live.js"></script>'
|
||||
heartbeat_ws_url = f"ws://{host}:50005/hbd"
|
||||
res = templates.TemplateResponse(
|
||||
"live.html ",
|
||||
{
|
||||
"title": "Heartbeat",
|
||||
"header": "Heartbeat",
|
||||
"heartbeat_ws_url": heartbeat_ws_url,
|
||||
"extra_scripts": extra_scripts,
|
||||
},
|
||||
)
|
||||
else:
|
||||
code, res = self.builderror(404, "Not Found", "requested URL was not found on this server.")
|
||||
|
||||
if "deflate" in rqAcceptEncoding:
|
||||
headerdict["Content-Encoding"] = "deflate"
|
||||
towrite = __import__("zlib").compress("\n".join(res).encode(), 6)
|
||||
else:
|
||||
towrite = "\n".join(res)
|
||||
headerdict["Content-Length"] = len(towrite)
|
||||
headerdict["Cache-Control"] = "private, must-revalidate, max-age=0"
|
||||
headerdict["Expires"] = "Thu, 01 Jan 1970 00:00:00 GMT"
|
||||
self.setheaders(200 if 'res' in locals() else code, headerdict)
|
||||
self.wfile.write(towrite if isinstance(towrite, bytes) else towrite.encode())
|
||||
|
||||
if xsig:
|
||||
# inform application via setting a flag on the server instance
|
||||
try:
|
||||
self.server.xsig = xsig
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return CustomHandler
|
||||
|
||||
Reference in New Issue
Block a user