Skip to content

Commit

Permalink
ensure HTTP_HOST is correctly defined (#807)
Browse files Browse the repository at this point in the history
* ensure HTTP_HOST is correctly defined

* remove print

* add even more fallbacks

* change admin host urlpattern order in prod

* do not alter host if already correct

* fix key error
  • Loading branch information
ludvigalden committed Mar 11, 2024
1 parent 48bf3a3 commit 0ae1614
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 27 deletions.
66 changes: 66 additions & 0 deletions src/admin/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -17,3 +18,68 @@ 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):
current_host = request.META.get('HTTP_HOST', '')
if (current_host and self.is_allowed_host(current_host)):
return self.get_response(request)

headers_to_check = [
('HTTP_X_FORWARDED_HOST', 'X-Forwarded-Host'),
('HTTP_HOST', 'Host'),
('HTTP_ORIGIN', 'Origin'),
('HTTP_REFERER', 'Referer')
]

host_name = None
for meta_key, header in headers_to_check:
# Prefer META if defined; fallback to request.headers
host_value = (request.META.get(meta_key)
or request.headers.get(header, ''))
# Extract the hostname, excluding protocol and path.
host_name = self.extract_hostname(host_value)
if host_name and self.is_allowed_host(host_name):
request.META['HTTP_HOST'] = host_name
break
host_name = None

if not host_name:
# Fallback to using host from request URL if no matching header
fallback_host = (request.build_absolute_uri()
.split('//')[1].split('/')[0])
fallback_host_name = self.extract_hostname(fallback_host)
if self.is_allowed_host(fallback_host_name):
request.META['HTTP_HOST'] = fallback_host_name

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
16 changes: 12 additions & 4 deletions src/admin/urls.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import absolute_import, unicode_literals

from django.conf import settings
from django.conf.urls import include, url

from wagtail.admin import urls as wagtailadmin_urls
Expand All @@ -10,7 +11,14 @@
urlpatterns = base_urlpatterns.copy()

# Insert `wagtailadmin_urls` at appropriate level
urlpatterns.insert(
len(urlpatterns) - 3,
url(r'', include(wagtailadmin_urls))
)
if settings.DEBUG:
# If DEBUG, two static file-urls are put at the end
urlpatterns.insert(
len(urlpatterns) - 3,
url(r'', include(wagtailadmin_urls))
)
else:
urlpatterns.insert(
len(urlpatterns) - 1,
url(r'', include(wagtailadmin_urls))
)
7 changes: 5 additions & 2 deletions src/moore/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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 = {
Expand Down
9 changes: 0 additions & 9 deletions src/moore/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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'
)
12 changes: 0 additions & 12 deletions src/moore/urls_utils.py

This file was deleted.

0 comments on commit 0ae1614

Please sign in to comment.