Files
heartbeat/hbd/http.py
T
2026-02-04 12:45:35 -05:00

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