import time from pathlib import Path import cherrypy import requests import config import ovenapi def check_webhook_throttle() -> bool: now = time.time() # Clean up notification list to recent notifications while config.NOTIFICATIONS and now - config.NOTIFICATIONS[0] > 60: config.NOTIFICATIONS.pop(0) config.NOTIFICATIONS.append(now) return not len(config.NOTIFICATIONS) > config.NOTIFICATION_THROTTLE def webhook_online(stream) -> None: if not config.is_webhook_ready(): return data = {"username": f"{config.WEBHOOK_NAME} Online", "content": config.WEBHOOK_ONLINE} if config.is_avatar_ready(): target_av = f"{stream[1]}/{stream[2]}.png" avatar = target_av if Path(config.WEBHOOK_AVATAR_PATH, target_av).is_file() else "default.png" data["avatar_url"] = f"{config.WEBHOOK_AVATAR_URL}/{avatar}" requests.post(config.WEBHOOK_URL, timeout=10, json=data, headers=config.WEBHOOK_HEADERS) def webhook_offline() -> None: if not config.is_webhook_ready(): return data = {"username": f"{config.WEBHOOK_NAME} Offline", "content": config.WEBHOOK_OFFLINE} if config.WEBHOOK_AVATAR_PATH and config.WEBHOOK_AVATAR_URL: data["avatar_url"] = f"{config.WEBHOOK_AVATAR_URL}/offline.png" requests.post(config.WEBHOOK_URL, timeout=10, json=data, headers=config.WEBHOOK_HEADERS) def check_authorized(host, app, stream, source) -> bool: # Are we globally disabled? if config.DISABLED: return False # IP Banned? if source in config.BLOCKED_IPS: return False # Nothing in the Oven API maps a domain to "default" vhost # So here we fudge checking default vhost for all apps/streams if f"default:{app}:{stream}" in config.DISABLED_KEYS: return False # Finally check the provided vhost app/stream return f"{host}:{app}:{stream}" not in config.DISABLED_KEYS @cherrypy.tools.register("on_end_request") def handle_notify() -> None: # If we don't have API creds we can't do this, abort if not (config.API_USER and config.API_PASS): return # Get stream list from API # Unfortunately Oven doesn't reflect the new stream fast enough so we have to wait :( time.sleep(1) stream_list = ovenapi.OvenAPI(config.API_USER, config.API_PASS).get_stream_list() # If we haven't gone empty->active or active->empty we need to do nothing if bool(stream_list) != bool(config.LAST_STREAM_LIST): if not check_webhook_throttle(): cherrypy.log("Webhook throttle limit hit, ignoring") return # Dispatch the appropriate webhook webhook_online(stream_list[0]) if stream_list else webhook_offline() # Save our stream list into a durable value config.LAST_STREAM_LIST = stream_list.copy() class Admission: # /admission to control/trigger sessions @cherrypy.expose @cherrypy.tools.json_in() @cherrypy.tools.json_out() @cherrypy.tools.handle_notify() def default(self) -> dict: # Fast fail if we have no json payload try: input_json = cherrypy.request.json except AttributeError: cherrypy.response.status = 400 return {} # If this is a viewer, allow it with no processing # This should never happen since we won't enable webhooks for viewing if input_json["request"]["direction"] == "outgoing": return {"allowed": True} # Figure out scheme, host, app and stream name for recording who is live _, _, host, app, path = input_json["request"]["url"].split("/")[:5] stream = path.split("?")[0] # If we are closing, return a fast 200 if input_json["request"]["status"] == "closing": return {} # Get client IP for ACL checking ip = input_json["client"]["real_ip"] # Check if stream is authorized if not check_authorized(host, app, stream, ip): cherrypy.log(f"Unauthorized stream key: {app}/{stream}") return {"allowed": False} # Compile and dispatch our response return {"allowed": True}