diff --git a/management.py b/management.py index c7d637b..493ec40 100644 --- a/management.py +++ b/management.py @@ -1,5 +1,6 @@ import subprocess from pathlib import Path +from urllib.parse import urlparse import cherrypy from mako.template import Template @@ -8,37 +9,65 @@ import config import ovenapi -@cherrypy.tools.register("on_end_request") -def restart_server() -> None: - subprocess.call(["sudo", "/usr/bin/systemctl", "restart", "ovenmediaengine"]) - - 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.OvenAPI(config.API_USER, 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 - @cherrypy.tools.restart_server() 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("Restart command dispatched") + 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: @@ -49,6 +78,9 @@ class Management: @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}") @@ -56,12 +88,18 @@ class Management: @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}") @@ -69,12 +107,18 @@ class Management: @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") diff --git a/viewer.py b/viewer.py index 10e8fe9..a38d8d3 100644 --- a/viewer.py +++ b/viewer.py @@ -1,4 +1,5 @@ from pathlib import Path +from urllib.parse import urlparse import cherrypy from mako.template import Template @@ -39,7 +40,7 @@ class Viewer: return "App not found" # Get domain for templates - domain = cherrypy.request.base.split("/")[-1].split(":")[0] + domain = urlparse(cherrypy.request.base).netloc # Any subpath is presumed to be a single player interface for app/stream if "page" in params: