Added streaming auth
This commit is contained in:
parent
c342c8b1e2
commit
2c60976525
|
|
@ -30,4 +30,9 @@ urlpatterns = [
|
|||
views.HealthCheck.as_view(),
|
||||
name="api-health",
|
||||
),
|
||||
path(
|
||||
"stream-auth/",
|
||||
views.StreamAuthView.as_view(),
|
||||
name="api-stream-auth",
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
"""all API views"""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import time
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from appsettings.src.config import ReleaseVersion
|
||||
from appsettings.src.reindex import ReindexProgress
|
||||
from common.serializers import (
|
||||
|
|
@ -18,6 +23,8 @@ from common.src.searching import SearchForm
|
|||
from common.src.ta_redis import RedisArchivist
|
||||
from common.src.watched import WatchState
|
||||
from common.views_base import AdminOnly, ApiBaseView
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
|
@ -208,3 +215,57 @@ class HealthCheck(APIView):
|
|||
def get(self, request):
|
||||
"""health check, no auth needed"""
|
||||
return Response("OK", status=200)
|
||||
|
||||
|
||||
class StreamAuthView(APIView):
|
||||
"""resolves to /api/stream-auth/
|
||||
Called by nginx auth_request to validate signed streaming URLs.
|
||||
Reads the original URI from X-Original-URI, extracts sig + expires
|
||||
query params, validates HMAC and Redis presence. No DRF auth.
|
||||
"""
|
||||
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
|
||||
def get(self, request):
|
||||
# pylint: disable=too-many-return-statements
|
||||
"""validate a signed stream URL"""
|
||||
original_uri = request.META.get("HTTP_X_ORIGINAL_URI", "")
|
||||
parsed = urlparse(original_uri)
|
||||
query = parse_qs(parsed.query)
|
||||
|
||||
sig = query.get("sig", [None])[0]
|
||||
expires_raw = query.get("expires", [None])[0]
|
||||
|
||||
if not sig or not expires_raw:
|
||||
return HttpResponse(status=403)
|
||||
|
||||
try:
|
||||
expires = int(expires_raw)
|
||||
except ValueError:
|
||||
return HttpResponse(status=403)
|
||||
|
||||
if expires < int(time.time()):
|
||||
return HttpResponse(status=403)
|
||||
|
||||
cached = RedisArchivist().get_message_dict(f"stream_token:{sig}")
|
||||
if not cached:
|
||||
return HttpResponse(status=403)
|
||||
|
||||
payload = (
|
||||
f"{cached['video_id']}:{expires}:{cached['user_id']}"
|
||||
)
|
||||
expected = hmac.new(
|
||||
settings.SECRET_KEY.encode(),
|
||||
payload.encode(),
|
||||
hashlib.sha256,
|
||||
).hexdigest()
|
||||
|
||||
if not hmac.compare_digest(sig, expected):
|
||||
return HttpResponse(status=403)
|
||||
|
||||
# Ensure the requested URI matches the signed video
|
||||
if f"/{cached['video_id']}." not in parsed.path:
|
||||
return HttpResponse(status=403)
|
||||
|
||||
return HttpResponse(status=200)
|
||||
|
|
|
|||
|
|
@ -30,4 +30,9 @@ urlpatterns = [
|
|||
views.VideoSimilarView.as_view(),
|
||||
name="api-video-similar",
|
||||
),
|
||||
path(
|
||||
"<slug:video_id>/stream-token/",
|
||||
views.StreamTokenView.as_view(),
|
||||
name="api-video-stream-token",
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
"""all API views for video endpoints"""
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import time
|
||||
|
||||
from common.serializers import ErrorResponseSerializer
|
||||
from common.src.helper import calc_is_watched
|
||||
from common.src.ta_redis import RedisArchivist
|
||||
from common.src.watched import WatchState
|
||||
from common.views_base import AdminWriteOnly, ApiBaseView
|
||||
from django.conf import settings
|
||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||
from playlist.src.index import YoutubePlaylist
|
||||
from rest_framework.response import Response
|
||||
|
|
@ -287,3 +292,49 @@ class VideoSimilarView(ApiBaseView):
|
|||
self.get_document_list(request, pagination=False)
|
||||
serializer = VideoSerializer(self.response["data"], many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class StreamTokenView(ApiBaseView):
|
||||
"""resolves to /api/video/<video_id>/stream-token/
|
||||
POST: mint a short-lived signed streaming token for VLC playback
|
||||
DELETE: revoke the active streaming token
|
||||
"""
|
||||
|
||||
search_base = "ta_video/_doc/"
|
||||
TTL_SECONDS = 300 # 5 minutes
|
||||
|
||||
def post(self, request, video_id):
|
||||
"""mint a streaming token"""
|
||||
self.get_document(video_id)
|
||||
if not self.response:
|
||||
error = ErrorResponseSerializer({"error": "video not found"})
|
||||
return Response(error.data, status=404)
|
||||
|
||||
expires = int(time.time()) + self.TTL_SECONDS
|
||||
user_id = request.user.id
|
||||
payload = f"{video_id}:{expires}:{user_id}"
|
||||
sig = hmac.new(
|
||||
settings.SECRET_KEY.encode(),
|
||||
payload.encode(),
|
||||
hashlib.sha256,
|
||||
).hexdigest()
|
||||
|
||||
RedisArchivist().set_message(
|
||||
f"stream_token:{sig}",
|
||||
{
|
||||
"video_id": video_id,
|
||||
"user_id": user_id,
|
||||
"expires": expires,
|
||||
},
|
||||
expire=self.TTL_SECONDS,
|
||||
)
|
||||
|
||||
return Response({"sig": sig, "expires": expires})
|
||||
|
||||
def delete(self, request, video_id):
|
||||
# pylint: disable=unused-argument
|
||||
"""revoke a streaming token"""
|
||||
sig = request.data.get("sig") or request.query_params.get("sig")
|
||||
if sig:
|
||||
RedisArchivist().del_message(f"stream_token:{sig}")
|
||||
return Response(status=204)
|
||||
|
|
|
|||
|
|
@ -26,13 +26,27 @@ server {
|
|||
}
|
||||
|
||||
location /youtube/ {
|
||||
auth_request /api/ping/;
|
||||
# Use signed-URL auth if ?sig= is present, otherwise session auth
|
||||
set $auth_endpoint /api/ping/;
|
||||
if ($arg_sig) {
|
||||
set $auth_endpoint /api/stream-auth/;
|
||||
}
|
||||
auth_request $auth_endpoint;
|
||||
alias /youtube/;
|
||||
types {
|
||||
video/mp4 mp4;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
location = /api/stream-auth/ {
|
||||
internal;
|
||||
include proxy_params;
|
||||
proxy_pass http://localhost:8080;
|
||||
proxy_pass_request_body off;
|
||||
proxy_set_header Content-Length "";
|
||||
proxy_set_header X-Original-URI $request_uri;
|
||||
}
|
||||
|
||||
location /api {
|
||||
include proxy_params;
|
||||
proxy_pass http://localhost:8080;
|
||||
|
|
|
|||
Loading…
Reference in New Issue