diff --git a/Dockerfile b/Dockerfile index af92f661662d08edf79e5615f12833aeb0d34f13..0ac59cabd4109dd15a888832ce38b284c415af58 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ FROM alpine:3.13.4 ENV PYTHONUNBUFFERED 1 ENV BUILDPKGS gettext py3-pip py3-wheel -ENV RUNTIMEPKGS uwsgi uwsgi-python python3 py3-psycopg2 py3-pyldap libmagic proj gdal +ENV RUNTIMEPKGS uwsgi uwsgi-python uwsgi-router_static python3 py3-psycopg2 py3-pyldap libmagic proj gdal RUN adduser -D -g '' uwsgi diff --git a/castellum/settings/default.py b/castellum/settings/default.py index 7a26d7db3d0619f826cdb05a50deea9fc0889fb6..e553c6513c027cf2a9c51df905638fbef2190470 100644 --- a/castellum/settings/default.py +++ b/castellum/settings/default.py @@ -180,6 +180,23 @@ MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media' +# By default, castellum does not serve media (aka "uploaded") files. +# You have 3 options here: +# +# - Set PROTECTED_MEDIA_SERVER to "django" (only for development) +# - Set PROTECTED_MEDIA_SERVER to None and let a proxy serve the files +# instead. This is insecure because it bypasses castellum login. +# - Set PROTECTED_MEDIA_SERVER to the proxy server you use (e.g. +# 'nginx', 'uwsgi') to use X-Sendfile. You will have to configure +# the proxy accordingly. This option is strongly recommended. +# +# references: +# - https://docs.djangoproject.com/en/3.2/ref/contrib/staticfiles/#django.contrib.staticfiles.views.serve +# - https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile/ +# - https://uwsgi-docs.readthedocs.io/en/latest/Snippets.html#x-sendfile-emulation +PROTECTED_MEDIA_SERVER = None +PROTECTED_MEDIA_URL = '/protected/' + NPM_ROOT_PATH = BASE_DIR.parent NPM_FILE_PATTERNS = { diff --git a/castellum/settings/development.py b/castellum/settings/development.py index 64f07f12d576e00c63388c169cd1205006a186a3..aa1c789277d5374cf8ebeec0ea8aabc596c8681c 100644 --- a/castellum/settings/development.py +++ b/castellum/settings/development.py @@ -7,6 +7,7 @@ SESSION_COOKIE_SECURE = False DEBUG = True +PROTECTED_MEDIA_SERVER = 'django' # setup debug toolbar diff --git a/castellum/urls.py b/castellum/urls.py index a3a26c2de879deb2c9f27949d15a41d545d9e31e..d47bdcd57251c4d8be540907e0853241ed64a1d9 100644 --- a/castellum/urls.py +++ b/castellum/urls.py @@ -19,8 +19,11 @@ # License along with Castellum. If not, see # . +import mimetypes + from django.conf import settings from django.contrib import admin +from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.views import LoginView from django.contrib.auth.views import LogoutView @@ -54,6 +57,24 @@ class FeedsView(LoginRequiredMixin, TemplateView): return context +@login_required +def protected_media(request, path): + if settings.PROTECTED_MEDIA_SERVER == 'django': + return serve(request, path, document_root=settings.MEDIA_ROOT) + elif settings.PROTECTED_MEDIA_SERVER == 'nginx': + response = HttpResponse() + response['X-Accel-Redirect'] = settings.PROTECTED_MEDIA_URL + path + return response + else: + mimetype, encoding = mimetypes.guess_type(settings.MEDIA_ROOT / path) + response = HttpResponse() + response['Content-Type'] = mimetype + if encoding: + response['Content-Encoding'] = encoding + response['X-Sendfile'] = settings.MEDIA_ROOT / path + return response + + def dummy(request): return HttpResponse('', status=204) @@ -84,9 +105,9 @@ urlpatterns = [ ), ] -if settings.DEBUG: +if settings.PROTECTED_MEDIA_SERVER: urlpatterns += [ - path('media/', serve, {'document_root': settings.MEDIA_ROOT}), + path('media/', protected_media), ] try: diff --git a/docs/example_deployment/settings.py b/docs/example_deployment/settings.py index c67bc88c5b26b84ff5e3cfec7fad62e3d6e55e68..cf6fcb87c63f1f5e150954de008bcf54422ce4a6 100644 --- a/docs/example_deployment/settings.py +++ b/docs/example_deployment/settings.py @@ -28,7 +28,8 @@ DATABASES = { ALLOWED_HOSTS = ['*'] -MEDIA_ROOT = '/media' +MEDIA_ROOT = Path('/media') +PROTECTED_MEDIA_SERVER = 'uwsgi' # The example deployment does not contain a mail server, so use dummy instead EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' diff --git a/docs/example_deployment/uwsgi.ini b/docs/example_deployment/uwsgi.ini index d9f1e02a53952fcdc65213a4847ad620b6164c1d..1127b3637a815aadc335716e7daf9107bf38e3b2 100644 --- a/docs/example_deployment/uwsgi.ini +++ b/docs/example_deployment/uwsgi.ini @@ -9,8 +9,14 @@ socket=0.0.0.0:8000 # enables internal http server and router protocol=http static-map=/static=/code/castellum/collected_static -static-map=/media=/media static-map=/favicon.ico=/code/castellum/collected_static/images/favicon.ico +# https://uwsgi-docs.readthedocs.io/en/latest/Snippets.html#x-sendfile-emulation +plugins = router_static +offload-threads = 2 +static-safe = /media +collect-header = X-Sendfile X_SENDFILE +response-route-if-not = empty:${X_SENDFILE} static:${X_SENDFILE} + wsgi-file=castellum/wsgi.py plugin=python