From 01523517d2e5f25e5cb1722048d95eefc59b0963 Mon Sep 17 00:00:00 2001 From: James Pace Date: Wed, 26 Aug 2026 14:16:58 +0000 Subject: [PATCH] Require login for video streams. --- am_i_up/Api.py | 43 ++++++++++++++++++++++++++++++-- am_i_up/Facts.py | 4 --- am_i_up/MediaMTXAuthenticator.py | 25 +++++++++++++++++++ params/mediamtx.yaml | 3 +++ 4 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 am_i_up/MediaMTXAuthenticator.py diff --git a/am_i_up/Api.py b/am_i_up/Api.py index be107b3..f4b97b6 100644 --- a/am_i_up/Api.py +++ b/am_i_up/Api.py @@ -12,13 +12,15 @@ from aiohttp import web import asyncio from ament_index_python import get_package_share_directory from am_i_up.Authenticator import Authenticator +from am_i_up.MediaMTXAuthenticator import MediaMTXAuthenticator import rclpy class Api: def __init__(self, facts): self._facts = facts - self._authenticator = Authenticator(facts.get_password(), facts.get_open_endpoints()) + self._authenticator = Authenticator(facts.get_password(), self.get_open_endpoints()) + self._mediamtx_authenticator = MediaMTXAuthenticator() async def run(self): ui_share_directory = get_package_share_directory(self._facts.get_ui_pkg()) @@ -34,10 +36,12 @@ class Api: web.get("/api/position", self.position), web.get("/api/diagnostics", self.diagnostics), web.post("/api/login", self.login), + web.post("/api/mediamtx/auth", self.mediamtx_auth), + web.get("/api/mediamtx/login", self.mediamtx_login), web.static("/assets", ui_static_directory), # we're not actually using key anywhere, but doing this allows react router # to work correctly. - web.get("/{key:.*}", self.index) + web.get("/{key}", self.index) ]) url = "0.0.0.0" @@ -53,6 +57,41 @@ class Api: await asyncio.sleep(3600) await runner.cleanup() + def get_open_endpoints(self): + return ["/login", "/api/login", "/assets/(.*)", "/api/mediamtx/auth"] + + async def mediamtx_auth(self, request): + # Take the mediamtx json and figure out if I'm a valid user. + # MediaMTX json looks like: + # { + # "user": "user", + # "password": "password", + # "token": "token", + # "ip": "ip", + # "action": "publish|read|playback|api|metrics|pprof", + # "path": "path", + # "protocol": "rtsp|rtmp|hls|webrtc|srt", + # "id": "id", + # "query": "query", + # "userAgent": "userAgent" + # } + # This route will be unprotected. + request_dict = await request.json() + print("Got request: {}", request_dict) + # Only allowing reads for now. + if request_dict["action"] != "read": + return web.Response(status=401) + valid_token = self._mediamtx_authenticator.confirm_token(request_dict["token"]) + if valid_token: + return web.Response(status=200) + return web.Response(status=401) + + async def mediamtx_login(self, request): + # Generate a valid token for the mediamtx stuff to use to login + # with. + token = self._mediamtx_authenticator.make_token() + resp = {"token": token} + return web.json_response(resp) async def login(self, request): request_dict = await request.json() diff --git a/am_i_up/Facts.py b/am_i_up/Facts.py index b43488d..e26ecc4 100644 --- a/am_i_up/Facts.py +++ b/am_i_up/Facts.py @@ -119,10 +119,6 @@ class Facts: def get_password(self): return self._password - def get_open_endpoints(self): - # Hardcoding for now, can make a parameter later. - return ["/login", "/api/login", "/assets/(.*)"] - def _status_callback(self, msg): self._status_string = msg.data diff --git a/am_i_up/MediaMTXAuthenticator.py b/am_i_up/MediaMTXAuthenticator.py new file mode 100644 index 0000000..e3a46f8 --- /dev/null +++ b/am_i_up/MediaMTXAuthenticator.py @@ -0,0 +1,25 @@ +from datetime import datetime, timedelta +import secrets + +class MediaMTXAuthenticator(): + def __init__(self): + # Type: Dict["token"] -> expiration time + self._valid_tokens = {} + + def confirm_token(self, token): + if not token in self._valid_tokens.keys(): + return False + token_expiration_time = self._valid_tokens[token] + now = datetime.now() + if now < token_expiration_time: + return True + return False + + def make_token(self): + token = secrets.token_urlsafe(32) + expiration_time = datetime.now() + timedelta(minutes=1) + self._valid_tokens[token] = expiration_time + + return token + + diff --git a/params/mediamtx.yaml b/params/mediamtx.yaml index c59e97b..be719ce 100644 --- a/params/mediamtx.yaml +++ b/params/mediamtx.yaml @@ -1,3 +1,6 @@ +authMethod: http +authHTTPAddress: http://127.0.0.1:8000/api/mediamtx/auth +authHTTPExclude: [] paths: image: source: rtsp://127.0.0.1:8559/image \ No newline at end of file