Require login for video streams.
This commit is contained in:
parent
52d2aa52e2
commit
01523517d2
|
|
@ -12,13 +12,15 @@ from aiohttp import web
|
||||||
import asyncio
|
import asyncio
|
||||||
from ament_index_python import get_package_share_directory
|
from ament_index_python import get_package_share_directory
|
||||||
from am_i_up.Authenticator import Authenticator
|
from am_i_up.Authenticator import Authenticator
|
||||||
|
from am_i_up.MediaMTXAuthenticator import MediaMTXAuthenticator
|
||||||
import rclpy
|
import rclpy
|
||||||
|
|
||||||
class Api:
|
class Api:
|
||||||
def __init__(self, facts):
|
def __init__(self, facts):
|
||||||
self._facts = 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):
|
async def run(self):
|
||||||
ui_share_directory = get_package_share_directory(self._facts.get_ui_pkg())
|
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/position", self.position),
|
||||||
web.get("/api/diagnostics", self.diagnostics),
|
web.get("/api/diagnostics", self.diagnostics),
|
||||||
web.post("/api/login", self.login),
|
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),
|
web.static("/assets", ui_static_directory),
|
||||||
# we're not actually using key anywhere, but doing this allows react router
|
# we're not actually using key anywhere, but doing this allows react router
|
||||||
# to work correctly.
|
# to work correctly.
|
||||||
web.get("/{key:.*}", self.index)
|
web.get("/{key}", self.index)
|
||||||
])
|
])
|
||||||
|
|
||||||
url = "0.0.0.0"
|
url = "0.0.0.0"
|
||||||
|
|
@ -53,6 +57,41 @@ class Api:
|
||||||
await asyncio.sleep(3600)
|
await asyncio.sleep(3600)
|
||||||
await runner.cleanup()
|
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):
|
async def login(self, request):
|
||||||
request_dict = await request.json()
|
request_dict = await request.json()
|
||||||
|
|
|
||||||
|
|
@ -119,10 +119,6 @@ class Facts:
|
||||||
def get_password(self):
|
def get_password(self):
|
||||||
return self._password
|
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):
|
def _status_callback(self, msg):
|
||||||
self._status_string = msg.data
|
self._status_string = msg.data
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
authMethod: http
|
||||||
|
authHTTPAddress: http://127.0.0.1:8000/api/mediamtx/auth
|
||||||
|
authHTTPExclude: []
|
||||||
paths:
|
paths:
|
||||||
image:
|
image:
|
||||||
source: rtsp://127.0.0.1:8559/image
|
source: rtsp://127.0.0.1:8559/image
|
||||||
Loading…
Reference in New Issue