diff --git a/docker/config/nginx/sapl.conf b/docker/config/nginx/sapl.conf index c47fd3075..89f3b3706 100644 --- a/docker/config/nginx/sapl.conf +++ b/docker/config/nginx/sapl.conf @@ -64,7 +64,7 @@ server { # Internal location used exclusively by X-Accel-Redirect responses # from serve_media(). Not reachable by external clients. - location /private/media/ { + location /internal/media/ { internal; alias /var/interlegis/sapl/media/; sendfile on; diff --git a/plan/RATE-LIMITER-PLAN.md b/plan/RATE-LIMITER-PLAN.md index a49a637a7..d82d3ac92 100644 --- a/plan/RATE-LIMITER-PLAN.md +++ b/plan/RATE-LIMITER-PLAN.md @@ -720,11 +720,11 @@ GET /media/foo.pdf │ ▼ serve_media(request, path='foo.pdf') - returns HttpResponse with X-Accel-Redirect: /private/media/foo.pdf + returns HttpResponse with X-Accel-Redirect: /internal/media/foo.pdf │ ▼ nginx sees X-Accel-Redirect header - /private/media/ internal location → reads file from disk → sends to client + /internal/media/ internal location → reads file from disk → sends to client ``` nginx does no routing beyond picking a `location` block. The mapping from @@ -757,7 +757,7 @@ location /media/ { } # Internal — only reachable via X-Accel-Redirect, not by external clients. -location /private/media/ { +location /internal/media/ { internal; alias /var/interlegis/sapl/media/; sendfile on; @@ -776,7 +776,7 @@ Per-request steps: 1. **Path traversal guard** — `os.path.abspath` check; raises 404 on escape. 2. **Auth gate** — `documentos_privados/` paths require an authenticated session; redirects to login otherwise. 3. **Path counter** — increments `rl:{ns}:path:{sha256}:reqs` in Redis DB 1 (TTL = `MEDIA_PATH_COUNTER_TTL`). -4. **Serve** — in DEBUG: `django.views.static.serve` directly. In production: `X-Accel-Redirect: /private/media/`. Nginx sets `Content-Type` from its own `mime.types`. +4. **Serve** — in DEBUG: `django.views.static.serve` directly. In production: `X-Accel-Redirect: /internal/media/`. Nginx sets `Content-Type` from its own `mime.types`. ### Settings diff --git a/sapl/base/media.py b/sapl/base/media.py index 6bc8f7eff..39258a2bf 100644 --- a/sapl/base/media.py +++ b/sapl/base/media.py @@ -15,7 +15,9 @@ Redis side-effects per request (DB 1, TTL=MEDIA_PATH_COUNTER_TTL): """ import hashlib +import mimetypes import os +from urllib.parse import quote from django.conf import settings from django.http import Http404, HttpResponse @@ -72,9 +74,11 @@ def serve_media(request, path): 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}' + filename = os.path.basename(path) + content_type, _ = mimetypes.guess_type(filename) + response = HttpResponse(content_type=content_type or 'application/octet-stream') + response['X-Accel-Redirect'] = f'/internal/media/{path}' + response['Content-Disposition'] = f"attachment; filename*=UTF-8''{quote(filename)}" response['Cache-Control'] = 'public, max-age=86400, stale-while-revalidate=3600' response['X-Robots-Tag'] = 'noindex' return response diff --git a/sapl/materia/views.py b/sapl/materia/views.py index f3c87a614..b6ef2d86c 100644 --- a/sapl/materia/views.py +++ b/sapl/materia/views.py @@ -55,7 +55,7 @@ from sapl.parlamentares.models import Legislatura from sapl.protocoloadm.models import Protocolo from sapl.settings import MAX_DOC_UPLOAD_SIZE, MEDIA_ROOT from sapl.utils import (autor_label, autor_modal, gerar_hash_arquivo, get_base_url, - get_mime_type_from_file_extension, lista_anexados, + lista_anexados, mail_service_configured, montar_row_autor, SEPARADOR_HASH_PROPOSICAO, show_results_filter_set, get_tempfile_dir, google_recaptcha_configured, MultiFormatOutputMixin) @@ -122,17 +122,7 @@ def proposicao_texto(request, pk): return redirect(reverse('sapl.materia:proposicao_detail', kwargs={'pk': pk})) - arquivo = proposicao.texto_original - - mime = get_mime_type_from_file_extension(arquivo.name) - - with open(arquivo.path, 'rb') as f: - data = f.read() - - response = HttpResponse(data, content_type='%s' % mime) - response['Content-Disposition'] = ( - 'inline; filename="%s"' % arquivo.name.split('/')[-1]) - return response + return redirect(proposicao.texto_original.url) logger.error('user=' + username + '. Objeto Proposicao com pk={} não encontrado.'.format(pk)) raise Http404 diff --git a/sapl/protocoloadm/views.py b/sapl/protocoloadm/views.py index 2eb7e6cd7..b681e0dd1 100755 --- a/sapl/protocoloadm/views.py +++ b/sapl/protocoloadm/views.py @@ -46,7 +46,7 @@ from sapl.protocoloadm.models import Protocolo, DocumentoAdministrativo,\ from sapl.relatorios.views import relatorio_doc_administrativos from sapl.middleware.ratelimit import get_client_ip, smart_key, smart_rate from sapl.utils import (create_barcode, get_base_url, - get_mime_type_from_file_extension, lista_anexados, + lista_anexados, show_results_filter_set, mail_service_configured, from_date_to_datetime_utc, google_recaptcha_configured, get_tempfile_dir, MultiFormatOutputMixin) @@ -102,28 +102,10 @@ def recuperar_materia_protocolo(request): def doc_texto_integral(request, pk): - can_see = True - - if not request.user.is_authenticated: - app_config = AppConfig.objects.last() - if app_config and app_config.documentos_administrativos == 'R': - can_see = False - - if can_see: - documento = DocumentoAdministrativo.objects.get(pk=pk) - if documento.texto_integral: - arquivo = documento.texto_integral - - mime = get_mime_type_from_file_extension(arquivo.name) - - with open(arquivo.path, 'rb') as f: - data = f.read() - - response = HttpResponse(data, content_type='%s' % mime) - response['Content-Disposition'] = ( - 'inline; filename="%s"' % arquivo.name.split('/')[-1]) - return response - raise Http404 + documento = get_object_or_404(DocumentoAdministrativo, pk=pk) + if not documento.texto_integral: + raise Http404 + return redirect(documento.texto_integral.url) def get_pdf_docacessorios(request, pk): @@ -516,9 +498,7 @@ class DocumentoAdministrativoCrud(Crud): def urlize(self, obj, fieldname): a = '%s' % ( - reverse( - 'sapl.protocoloadm:doc_texto_integral', - kwargs={'pk': obj.pk}), + obj.texto_integral.url, obj.texto_integral.name.split('/')[-1]) return obj.texto_integral.field.verbose_name, a diff --git a/sapl/utils.py b/sapl/utils.py index 5c35f959e..191ea87a0 100644 --- a/sapl/utils.py +++ b/sapl/utils.py @@ -78,7 +78,6 @@ def is_weak_password(password): return len(password) < MIN_PASSWORD_LENGTH or not (pwd_has_lowercase and pwd_has_uppercase and pwd_has_number and pwd_has_special_char) - def groups_remove_user(user, groups_name): from django.contrib.auth.models import Group