Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge #807 into master #808

Merged
merged 6 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions src/admin/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from django.conf import settings
from django_hosts.resolvers import reverse_host
from django.http import HttpResponseRedirect, HttpResponse
from django.http import HttpRequest
from django.utils.deprecation import MiddlewareMixin
import re


class RedirectAdminMiddleware(MiddlewareMixin):
def process_request(self, request: HttpRequest) -> HttpResponse:
if (request.path.startswith('/admin/') and
request.method == 'GET' and
request.headers.get('Accept', '').startswith('text/html')):
protocol = 'https' if request.is_secure() else 'http'
host = reverse_host('admin')
if getattr(settings, 'HOST_PORT', None):
host = f"{host}:{settings.HOST_PORT}"
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))
)
16 changes: 0 additions & 16 deletions src/admin/views.py

This file was deleted.

5 changes: 5 additions & 0 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,6 +95,8 @@
'django.middleware.locale.LocaleMiddleware',

'wagtail.contrib.redirects.middleware.RedirectMiddleware',
# Redirect GET html at path "/admin/*" admin subdomain
'admin.middleware.RedirectAdminMiddleware',
# Subdomain for admin site. Needed by django_hosts
'django_hosts.middleware.HostsResponseMiddleware',
]
Expand Down
18 changes: 1 addition & 17 deletions src/moore/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@

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

from search import views as search_views
from wagtail.core import urls as wagtail_urls
from wagtail.documents import urls as wagtaildocs_urls
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
from admin.views import redirect_admin

urlpatterns = [
# Needs to be imported before wagtail urls
Expand All @@ -23,12 +21,6 @@
url(r'', include('events.urls')),
path('member_check_api/', member_check_api, name='member_check_api'),

re_path(
r'^admin/(?P<path>.*)$',
redirect_admin,
name='wagtailadmin_redirect'
),

url(r'^documents/', include(wagtaildocs_urls)),

url(r'^search/$', search_views.search, name='search'),
Expand Down Expand Up @@ -60,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.