237 lines
9.6 KiB
Python
237 lines
9.6 KiB
Python
"""HTTP server and handler scaffolds (thin wrappers around http.server)."""
|
|
from http import server
|
|
import json
|
|
import time
|
|
import urllib.parse
|
|
|
|
from urllib3 import request
|
|
from fastapi.templating import Jinja2Templates
|
|
|
|
class HttpServer(server.ThreadingHTTPServer):
|
|
allow_reuse_address = True
|
|
|
|
def threaded(self):
|
|
pass
|
|
|
|
|
|
def make_handler_class(
|
|
config,
|
|
hbdclass,
|
|
msgs_getter,
|
|
log=None,
|
|
email=None,
|
|
pushmsg=None,
|
|
msg_to_websockets=None,
|
|
tcss=None,
|
|
DEBUG=0,
|
|
verbose=False,
|
|
get_now=None,
|
|
VER="",
|
|
):
|
|
"""Return a BaseHTTPRequestHandler subclass bound to runtime objects.
|
|
|
|
`msgs_getter` should be a callable that returns a list-like of messages.
|
|
"""
|
|
templates = Jinja2Templates(directory="templates")
|
|
get_now = get_now or (lambda: time.time())
|
|
|
|
class CustomHandler(server.BaseHTTPRequestHandler):
|
|
|
|
server_version = f"HeartbeatHTTP/{VER}"
|
|
|
|
def version_string(self):
|
|
return self.server_version
|
|
|
|
def handle(self):
|
|
try:
|
|
return server.BaseHTTPRequestHandler.handle(self)
|
|
except Exception as e:
|
|
self.log_error("Request went away: %r", e)
|
|
self.close_connection = 1
|
|
return
|
|
|
|
def do_HEAD(self):
|
|
self.setheaders(200)
|
|
|
|
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()
|
|
|
|
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
|
|
|
|
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
|
|
|
|
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
|
|
|
|
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)
|
|
|
|
if qr.path == "/":
|
|
res = self.buildpage()
|
|
|
|
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))
|
|
|
|
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
|