"""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('') res.append("") res.append("") res.append("%s" % (title)) if refresh: res.append("\n" % refresh) if extras: res.append(extras) res.append("") res.append('') return res def buildpage(self): res = self.buildhead(refresh=60, extras=tcss) res.append("

Heartbeat status %s

" % VER) res += hbdclass.ubHost.buildhosttable() res += hbdclass.ubHost.buildmsgtable(msgs_getter()) res.append( "

%s (%s)

" % (time.strftime("%H:%M:%S", time.localtime(get_now())), config.get("tz", "CET-1CDT")) ) res.append("") return res def builderror(self, code, cause, lcause): res = [] res.append('') res.append("") res.append("%s %s" % (code, cause)) res.append("") res.append("

%s

" % (cause)) res.append("

%s

" % lcause) res.append("
") res.append( "
hbd (Unix) Server at %s:%s
" % (config.get("hbd_host"), config.get("hbd_port")) ) res.append("") 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
" % (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 = '' # '' 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