mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-06-06 17:13:02 -04:00
feat: introduce x-accell headers
This commit is contained in:
@@ -202,6 +202,15 @@ THUMBNAIL_UPLOAD_DIR = f"{MEDIA_UPLOAD_DIR}/thumbnails/"
|
||||
SUBTITLES_UPLOAD_DIR = f"{MEDIA_UPLOAD_DIR}/subtitles/"
|
||||
HLS_DIR = os.path.join(MEDIA_ROOT, "hls/")
|
||||
|
||||
# Protect media files via nginx auth_request
|
||||
# When True, nginx delegates authorization for /media/<protected>/... to a
|
||||
# Django endpoint that checks the Media's state and the user's access.
|
||||
USE_X_ACCEL_REDIRECT = True
|
||||
# Subdirectories of MEDIA_ROOT that should be gated. "chunks" is intentionally
|
||||
# omitted (upload state, not playback).
|
||||
X_ACCEL_PROTECTED_PATHS = ["encoded", "hls", "original"]
|
||||
X_ACCEL_AUTH_CACHE_SECONDS = 300
|
||||
|
||||
FFMPEG_COMMAND = "ffmpeg" # this is the path
|
||||
FFPROBE_COMMAND = "ffprobe" # this is the path
|
||||
MP4HLS = "mp4hls"
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
VERSION = "8.0.8"
|
||||
VERSION = "8.1.0"
|
||||
|
||||
@@ -15,9 +15,27 @@ server {
|
||||
location /static {
|
||||
alias /home/mediacms.io/mediacms/static ;
|
||||
}
|
||||
location = /_media_auth {
|
||||
internal;
|
||||
proxy_pass http://127.0.0.1:9000/api/v1/media-auth;
|
||||
proxy_pass_request_body off;
|
||||
proxy_set_header Content-Length "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Original-URI $request_uri;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $forwarded_proto;
|
||||
proxy_set_header Cookie $http_cookie;
|
||||
}
|
||||
|
||||
location /media/original {
|
||||
alias /home/mediacms.io/mediacms/media_files/original;
|
||||
location ~ ^/media/(encoded|hls|original)/(.*)$ {
|
||||
auth_request /_media_auth;
|
||||
alias /home/mediacms.io/mediacms/media_files/$1/$2;
|
||||
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
|
||||
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
|
||||
}
|
||||
|
||||
location /media {
|
||||
|
||||
@@ -106,6 +106,7 @@ urlpatterns = [
|
||||
re_path(r"^api/v1/tasks$", views.TasksList.as_view()),
|
||||
re_path(r"^api/v1/tasks/$", views.TasksList.as_view()),
|
||||
re_path(r"^api/v1/tasks/(?P<friendly_token>[\w|\W]*)$", views.TaskDetail.as_view()),
|
||||
re_path(r"^api/v1/media-auth$", views.media_auth, name="media_auth"),
|
||||
re_path(r"^manage/comments$", views.manage_comments, name="manage_comments"),
|
||||
re_path(r"^manage/media$", views.manage_media, name="manage_media"),
|
||||
re_path(r"^manage/users$", views.manage_users, name="manage_users"),
|
||||
|
||||
@@ -10,6 +10,7 @@ from .media import MediaDetail # noqa: F401
|
||||
from .media import MediaList # noqa: F401
|
||||
from .media import MediaSearch # noqa: F401
|
||||
from .media import media_share # noqa: F401
|
||||
from .media_auth import media_auth # noqa: F401
|
||||
from .pages import about # noqa: F401
|
||||
from .pages import add_subtitle # noqa: F401
|
||||
from .pages import approval_required # noqa: F401
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.http import HttpResponse
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_GET
|
||||
|
||||
from ..methods import is_mediacms_editor
|
||||
from ..models import Media
|
||||
|
||||
UID_RE = re.compile(r"[0-9a-f]{32}")
|
||||
|
||||
|
||||
def _ttl():
|
||||
return getattr(settings, "X_ACCEL_AUTH_CACHE_SECONDS", 300)
|
||||
|
||||
|
||||
def _extract_uid(uri):
|
||||
if not uri:
|
||||
return None
|
||||
match = UID_RE.search(uri)
|
||||
return match.group(0) if match else None
|
||||
|
||||
|
||||
def _lookup_state(uid):
|
||||
"""Return (state, owner_id) for a uid, or (None, None) if missing.
|
||||
|
||||
Cached on uid alone since state/ownership do not depend on the requester.
|
||||
Uses .values() rather than .only() because Media.__init__ touches deferred
|
||||
file fields, which would otherwise recurse via refresh_from_db.
|
||||
"""
|
||||
state_key = f"xaccel:state:{uid}"
|
||||
cached = cache.get(state_key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
row = Media.objects.filter(uid=uid).values("state", "user_id").first()
|
||||
value = (row["state"], row["user_id"]) if row else (None, None)
|
||||
cache.set(state_key, value, _ttl())
|
||||
return value
|
||||
|
||||
|
||||
def _decide(uid, user):
|
||||
state, owner_id = _lookup_state(uid)
|
||||
if state is None:
|
||||
return False
|
||||
if state in ("public", "unlisted"):
|
||||
return True
|
||||
# private
|
||||
if not user.is_authenticated:
|
||||
return False
|
||||
if owner_id == user.id:
|
||||
return True
|
||||
if is_mediacms_editor(user):
|
||||
return True
|
||||
# RBAC / MediaPermission path needs a full Media instance.
|
||||
try:
|
||||
media = Media.objects.get(uid=uid)
|
||||
except Media.DoesNotExist:
|
||||
return False
|
||||
return user.has_member_access_to_media(media)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_GET
|
||||
def media_auth(request):
|
||||
"""Authorize a protected media request from nginx auth_request.
|
||||
|
||||
nginx passes the original request URI in the X-Original-URI header. The
|
||||
Media.uid (32 hex chars, no dashes) is embedded somewhere in that URI for
|
||||
every protected path. No uid => deny. Unknown uid => deny.
|
||||
"""
|
||||
if not getattr(settings, "USE_X_ACCEL_REDIRECT", True):
|
||||
return HttpResponse(status=204)
|
||||
|
||||
uri = request.META.get("HTTP_X_ORIGINAL_URI", "")
|
||||
uid = _extract_uid(uri)
|
||||
if not uid:
|
||||
return HttpResponse(status=403)
|
||||
|
||||
user = request.user
|
||||
cache_key = f"xaccel:auth:{uid}:{user.id if user.is_authenticated else 'anon'}"
|
||||
cached = cache.get(cache_key)
|
||||
if cached is None:
|
||||
allowed = _decide(uid, user)
|
||||
cache.set(cache_key, allowed, _ttl())
|
||||
else:
|
||||
allowed = cached
|
||||
|
||||
return HttpResponse(status=204 if allowed else 403)
|
||||
Reference in New Issue
Block a user