From 236b86d46f7b363c1affdaa255633258cc4e7bb5 Mon Sep 17 00:00:00 2001 From: ludvigalden Date: Sat, 9 Mar 2024 11:46:50 +0100 Subject: [PATCH] ensure HTTP_HOST is correctly defined --- src/admin/middleware.py | 52 ++++++++++++++++++++++++++++++++++++++ src/moore/settings/base.py | 7 +++-- src/moore/urls.py | 9 ------- src/moore/urls_utils.py | 12 --------- 4 files changed, 57 insertions(+), 23 deletions(-) delete mode 100644 src/moore/urls_utils.py diff --git a/src/admin/middleware.py b/src/admin/middleware.py index a0e15717..4dca81b7 100644 --- a/src/admin/middleware.py +++ b/src/admin/middleware.py @@ -3,6 +3,7 @@ from django.http import HttpResponseRedirect, HttpResponse from django.http import HttpRequest from django.utils.deprecation import MiddlewareMixin +import re class RedirectAdminMiddleware(MiddlewareMixin): @@ -17,3 +18,54 @@ def process_request(self, request: HttpRequest) -> HttpResponse: path = request.path[7:] # Remove '/admin/' return HttpResponseRedirect(f'{protocol}://{host}/{path}') return None + + +class TrustedHostMiddleware(MiddlewareMixin): + """ + Middleware to dynamically set the HTTP_HOST from a set of HTTP headers, + ensuring it matches the allowed hosts in settings.ALLOWED_HOSTS. + """ + + def __call__(self, request): + # Headers to check for the host value, in priority order. + # X-Forwarded-Host is typically set by proxies. + # HTTP_HOST is the standard header sent by clients. + # HTTP_ORIGIN and HTTP_REFERER can offer fallbacks. + headers_to_check = [ + 'HTTP_X_FORWARDED_HOST', 'HTTP_HOST', 'HTTP_ORIGIN', 'HTTP_REFERER' + ] + + for header in headers_to_check: + host_value = request.META.get(header, '') + # Extract the hostname, excluding protocol and path. + host_name = self.extract_hostname(host_value) + # If extracted host is allowed, set it as HTTP_HOST + if self.is_allowed_host(host_name): + request.META['HTTP_HOST'] = host_name + print("HOST", host_name, header) + break + return self.get_response(request) + + def extract_hostname(self, value): + """ + Extracts hostname from a URL, ignoring protocol, port, and path. + """ + # Regex captures hostname from start of string or after protocol. + # It stops at port delimiter ":" or path delimiter "/". + match = re.search(r'^(?:https?://)?([^:/]+)', value) + return match.group(1) if match else '' + + def is_allowed_host(self, host): + """ + Checks if a host matches any pattern in ALLOWED_HOSTS. + """ + # Loop through ALLOWED_HOSTS to see if the host matches any pattern. + # Patterns starting with '.' match any subdomain. + for allowed_host in settings.ALLOWED_HOSTS: + if allowed_host.startswith('.'): + domain = allowed_host[1:] + if host.endswith('.' + domain) or host == domain[1:]: + return True + elif host == allowed_host: # Exact match + return True + return False diff --git a/src/moore/settings/base.py b/src/moore/settings/base.py index 27eb3945..efd822c0 100644 --- a/src/moore/settings/base.py +++ b/src/moore/settings/base.py @@ -80,6 +80,9 @@ ] MIDDLEWARE = [ + # Make sure the HTTP_HOST is set correctly – djang-hosts uses get_host() + # https://docs.djangoproject.com/en/5.0/ref/request-response/#django.http.HttpRequest.get_host + 'admin.middleware.TrustedHostMiddleware', # Subdomain for admin site. Needed by django_hosts 'django_hosts.middleware.HostsRequestMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -92,10 +95,10 @@ 'django.middleware.locale.LocaleMiddleware', 'wagtail.contrib.redirects.middleware.RedirectMiddleware', - # Subdomain for admin site. Needed by django_hosts - 'django_hosts.middleware.HostsResponseMiddleware', # Redirect GET html at path "/admin/*" admin subdomain 'admin.middleware.RedirectAdminMiddleware', + # Subdomain for admin site. Needed by django_hosts + 'django_hosts.middleware.HostsResponseMiddleware', ] DATABASES = { diff --git a/src/moore/urls.py b/src/moore/urls.py index 8034d236..fab843a0 100644 --- a/src/moore/urls.py +++ b/src/moore/urls.py @@ -10,7 +10,6 @@ from wagtail.admin import urls as wagtailadmin_urls from .api import api_router -from .urls_utils import delete_urls from members.views import member_check_api @@ -53,11 +52,3 @@ settings.MEDIA_URL, document_root=settings.MEDIA_ROOT ) - -# We remove the /admin redirect -# if running tests in order to make writing tests easier. -if settings.IS_RUNNING_TEST: - urlpatterns = delete_urls( - urlpatterns, - delete_name='wagtailadmin_redirect' - ) diff --git a/src/moore/urls_utils.py b/src/moore/urls_utils.py deleted file mode 100644 index 29ad482a..00000000 --- a/src/moore/urls_utils.py +++ /dev/null @@ -1,12 +0,0 @@ -from __future__ import absolute_import, unicode_literals - - -def delete_urls(urlpatterns: list, delete_name: str): - for index, pattern in enumerate(urlpatterns): - if hasattr(pattern, 'name'): - if pattern.name == delete_name: - # Insert before index - urlpatterns.pop(index) - break - - return urlpatterns