mirror of
https://github.com/mediacms-io/mediacms.git
synced 2026-06-07 01:14:19 -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/"
|
SUBTITLES_UPLOAD_DIR = f"{MEDIA_UPLOAD_DIR}/subtitles/"
|
||||||
HLS_DIR = os.path.join(MEDIA_ROOT, "hls/")
|
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
|
FFMPEG_COMMAND = "ffmpeg" # this is the path
|
||||||
FFPROBE_COMMAND = "ffprobe" # this is the path
|
FFPROBE_COMMAND = "ffprobe" # this is the path
|
||||||
MP4HLS = "mp4hls"
|
MP4HLS = "mp4hls"
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
VERSION = "8.0.8"
|
VERSION = "8.1.0"
|
||||||
|
|||||||
@@ -15,9 +15,27 @@ server {
|
|||||||
location /static {
|
location /static {
|
||||||
alias /home/mediacms.io/mediacms/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 {
|
location ~ ^/media/(encoded|hls|original)/(.*)$ {
|
||||||
alias /home/mediacms.io/mediacms/media_files/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 {
|
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/$", 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/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/comments$", views.manage_comments, name="manage_comments"),
|
||||||
re_path(r"^manage/media$", views.manage_media, name="manage_media"),
|
re_path(r"^manage/media$", views.manage_media, name="manage_media"),
|
||||||
re_path(r"^manage/users$", views.manage_users, name="manage_users"),
|
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 MediaList # noqa: F401
|
||||||
from .media import MediaSearch # noqa: F401
|
from .media import MediaSearch # noqa: F401
|
||||||
from .media import media_share # 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 about # noqa: F401
|
||||||
from .pages import add_subtitle # noqa: F401
|
from .pages import add_subtitle # noqa: F401
|
||||||
from .pages import approval_required # 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