mirror of https://github.com/interlegis/sapl.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
80 lines
2.8 KiB
80 lines
2.8 KiB
"""
|
|
serve_media — X-Accel-Redirect gate for all /media/ files.
|
|
|
|
Production flow (nginx proxies /media/ to Gunicorn):
|
|
1. Django middleware runs (IP rate-limit, bot UA check, etc.).
|
|
2. serve_media() runs auth check for documentos_privados/, writes
|
|
URL-path counter to Redis DB 1, then returns X-Accel-Redirect.
|
|
Nginx serves the bytes directly from disk — Gunicorn worker freed immediately.
|
|
|
|
Development flow (DEBUG=True, nginx absent):
|
|
Falls back to django.views.static.serve for live file serving.
|
|
|
|
Redis side-effects per request (DB 1, TTL=MEDIA_PATH_COUNTER_TTL):
|
|
rl:{ns}:path:{sha256('/media/<path>')}:reqs — URL-path access counter
|
|
"""
|
|
|
|
import hashlib
|
|
import os
|
|
|
|
from django.conf import settings
|
|
from django.http import Http404, HttpResponse
|
|
from django.views.static import serve
|
|
|
|
from sapl import settings as sapl_settings
|
|
from sapl.middleware.ratelimit import (
|
|
_NAMESPACE,
|
|
RL_PATH_REQUESTS,
|
|
_incr_with_ttl,
|
|
)
|
|
|
|
|
|
def _safe_resolve(rel_path):
|
|
"""
|
|
Return the absolute path of rel_path inside MEDIA_ROOT.
|
|
Raises Http404 if the resolved path would escape the root
|
|
(path traversal guard).
|
|
"""
|
|
abs_root = os.path.abspath(settings.MEDIA_ROOT)
|
|
abs_path = os.path.abspath(os.path.join(abs_root, rel_path))
|
|
if not abs_path.startswith(abs_root + os.sep) and abs_path != abs_root:
|
|
raise Http404
|
|
return abs_path
|
|
|
|
|
|
def serve_media(request, path):
|
|
"""
|
|
Registered in sapl/urls.py for both DEBUG and production.
|
|
Route: ^media/(?P<path>.*)$
|
|
"""
|
|
# Path traversal guard — raises Http404 on escape attempt.
|
|
abs_path = _safe_resolve(path)
|
|
|
|
# Auth gate for private documents — redirect to login if anonymous.
|
|
if path.startswith('documentos_privados/'):
|
|
user = getattr(request, 'user', None)
|
|
if user is None or not user.is_authenticated:
|
|
from django.contrib.auth.views import redirect_to_login
|
|
return redirect_to_login(request.get_full_path())
|
|
|
|
# 404 before writing any counters.
|
|
if not os.path.isfile(abs_path):
|
|
raise Http404
|
|
|
|
# URL-path counter (DB 1).
|
|
_incr_with_ttl(
|
|
RL_PATH_REQUESTS.format(ns=_NAMESPACE, sha256=hashlib.sha256(f'/media/{path}'.encode()).hexdigest()),
|
|
ttl=sapl_settings.MEDIA_PATH_COUNTER_TTL,
|
|
)
|
|
|
|
if settings.DEBUG:
|
|
# Development: no nginx present; serve the file directly.
|
|
return serve(request, path, document_root=settings.MEDIA_ROOT)
|
|
|
|
# Production: tell nginx to serve the file from the internal location.
|
|
# Nginx sets Content-Type from its own mime.types when serving the file.
|
|
response = HttpResponse()
|
|
response['X-Accel-Redirect'] = f'/private/media/{path}'
|
|
response['Cache-Control'] = 'public, max-age=86400, stale-while-revalidate=3600'
|
|
response['X-Robots-Tag'] = 'noindex'
|
|
return response
|
|
|