import subprocess from pathlib import Path from urllib.parse import urlparse import cherrypy from mako.template import Template import config import ovenapi class Management: def __init__(self): self.page_template = Path("template/management.mako").read_text(encoding="utf-8") self.redirect_template = Path("template/message.mako").read_text(encoding="utf-8") self.api = ovenapi.get_api_handle(username=config.API_USER, password=config.API_PASS) @staticmethod def __verify_same_domain() -> bool: """ Verify that the requested domain and referer domain match. This is mainly intended to be used as a guard for "destructive" endpoints such as /management/restart. This safeguards against cross-site requests as well as accidental history completions such as intents to access /management but one's browser helpfully populates /management/restart. """ referer = cherrypy.request.headers.get("Referer", "").lower() # For comparing request and referer domains we drop the port referer_domain = urlparse(referer).netloc.split(":")[0] request_domain = urlparse(cherrypy.request.base).netloc.split(":")[0] return referer_domain == request_domain @staticmethod def __restart_server() -> None: subprocess.call(["sudo", "/usr/bin/systemctl", "restart", "ovenmediaengine"]) def __message_and_redirect(self, message: str) -> bytes | str: return Template(self.redirect_template).render(message=message) @cherrypy.expose def restart(self) -> bytes | str: if not self.__verify_same_domain(): cherrypy.response.status = 403 return "Cross-site request detected. Please go back to /management and try again" # Blank our stream list because we're about to DC everyone config.LAST_STREAM_LIST = [] self.__restart_server() # Compile and dispatch our response return self.__message_and_redirect("Server restarted") @cherrypy.expose def disconnect(self, target): if not self.__verify_same_domain(): cherrypy.response.status = 403 return "Cross-site request detected. Please go back to /management and try again" vhost, app, stream = target.split(":") self.api.disconnect_key(vhost, app, stream) return self.__message_and_redirect(f"Disconnected {target}") @cherrypy.expose def ban(self, target): if not self.__verify_same_domain(): cherrypy.response.status = 403 return "Cross-site request detected. Please go back to /management and try again" vhost, app, stream = target.split(":") ip = self.api.get_stream_ip(vhost, app, stream) if ip: config.BLOCKED_IPS.append(ip) self.disconnect(target) return self.__message_and_redirect(f"Banned {ip}") return self.__message_and_redirect("No stream found at that location or other error") @cherrypy.expose def unban(self, target): if not self.__verify_same_domain(): cherrypy.response.status = 403 return "Cross-site request detected. Please go back to /management and try again" if target in config.BLOCKED_IPS: config.BLOCKED_IPS.remove(target) return self.__message_and_redirect(f"Unbanned {target}") return self.__message_and_redirect(f"{target} not in ban list") @cherrypy.expose def disable(self, target): if not self.__verify_same_domain(): cherrypy.response.status = 403 return "Cross-site request detected. Please go back to /management and try again" config.DISABLED_KEYS.append(target) self.disconnect(target) return self.__message_and_redirect(f"Disabled key {target}") @cherrypy.expose def enable(self, target): if not self.__verify_same_domain(): cherrypy.response.status = 403 return "Cross-site request detected. Please go back to /management and try again" if target in config.DISABLED_KEYS: config.DISABLED_KEYS.remove(target) return self.__message_and_redirect(f"Re-enabled {target}") return self.__message_and_redirect(f"{target} not in disabled key list") @cherrypy.expose def stop(self): if not self.__verify_same_domain(): cherrypy.response.status = 403 return "Cross-site request detected. Please go back to /management and try again" config.DISABLED = True self.api.disconnect_all() return self.__message_and_redirect("Server disabled") @cherrypy.expose def start(self): if not self.__verify_same_domain(): cherrypy.response.status = 403 return "Cross-site request detected. Please go back to /management and try again" config.DISABLED = False return self.__message_and_redirect("Server re-enabled") @cherrypy.expose def default(self) -> bytes | str: if not (config.API_USER and config.API_PASS): cherrypy.response.status = 503 return "Remote management is disabled on this node." data = self.api.get_all_stream_info() return Template(self.page_template).render( DISABLED=config.DISABLED, BLOCKED_IPS=config.BLOCKED_IPS, DISABLED_KEYS=config.DISABLED_KEYS, STREAM_LIST=config.LAST_STREAM_LIST, data=data, )