Skip to content

Commit

Permalink
Merge pull request #4 from PetrDlouhy/domain_exempt
Browse files Browse the repository at this point in the history
change CANONICAL_DOMAIN_EXCEPTION->CANONICAL_DOMAIN_EXEMPT, respect SECURE_REDIRECT_EXEMPT
  • Loading branch information
matthiask authored Aug 8, 2024
2 parents 05db61f + 9e9e879 commit ff7266a
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 22 deletions.
49 changes: 33 additions & 16 deletions canonical_domain/middleware.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import re

from django.apps import apps
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
Expand All @@ -13,11 +15,17 @@

def canonical_domain(get_response):
host = getattr(settings, "SECURE_SSL_HOST", "")
secure = getattr(settings, "SECURE_SSL_REDIRECT", False)
exceptions = getattr(
# List of complete domains such as 'api.example.com'
settings, "CANONICAL_DOMAIN_EXCEPTIONS", []
)
secure_redirect = getattr(settings, "SECURE_SSL_REDIRECT", False)

# List of complete domains such as r'^api.example.com$'
host_exempt = [
re.compile(r) for r in getattr(settings, "CANONICAL_DOMAIN_EXEMPT", [])
]

# List of regex patterns for paths that should not be redirected
path_exempt = [
re.compile(r) for r in getattr(settings, "SECURE_REDIRECT_EXEMPT", [])
]

if not host:
return get_response
Expand All @@ -33,25 +41,34 @@ def middleware(request):
}:
return get_response(request)

path = request.path.lstrip("/")
secure_changes = not any(pattern.search(path) for pattern in path_exempt)
is_secure = (
(secure_redirect or request.is_secure())
if secure_changes else
request.is_secure()
)

matches = request.get_host() == host

if matches and secure and request.is_secure():
return get_response(request)
elif matches and not secure:
if matches and (
(secure_redirect and request.is_secure())
or not secure_redirect
or not secure_changes
):
return get_response(request)

for exception in exceptions:
if request.get_host() == exception:
if secure and not request.is_secure():
return HttpResponsePermanentRedirect(
"https://%s%s" % (request.get_host(), request.get_full_path())
)
return get_response(request)
if any(pattern.search(request.get_host()) for pattern in host_exempt):
if secure_redirect and not request.is_secure():
return HttpResponsePermanentRedirect(
"https://%s%s" % (request.get_host(), request.get_full_path())
)
return get_response(request)

return HttpResponsePermanentRedirect(
"http%s://%s%s"
% (
"s" if (secure or request.is_secure()) else "",
"s" if is_secure else "",
host,
request.get_full_path(),
)
Expand Down
12 changes: 9 additions & 3 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,23 @@ Installation and usage
- Set ``SECURE_SSL_HOST = 'example.com'`` in your settings.
- Optionally set ``SECURE_SSL_REDIRECT = True`` if you want to
enforce HTTPS.
- ``django-canonical-domain`` also respects ``SECURE_REDIRECT_EXEMPT`` settings.
In the case path matches the regex the url will be redirected to ``SECURE_SSL_HOST``,
but the protocol will not be changed.



Configuration
#############


``CANONICAL_DOMAIN_EXCEPTIONS``
``CANONICAL_DOMAIN_EXEMPT``

Default: []

A list of complete domain names such as ``'api.example.com'`` that should
not be redirected to the canonical domain.
A list of complete domain regex matches such, e.g. ``CANONICAL_DOMAIN_EXEMPT = [r'^api.example.com$', ...]``

When the host matches any of these, the middleware will not redirect to the canonical domain.


.. include:: ../CHANGELOG.rst
40 changes: 38 additions & 2 deletions tests/testapp/test_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def test_https_requests(self):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b"Hello world")

@override_settings(CANONICAL_DOMAIN_EXCEPTIONS=["api.example.com"])
@override_settings(CANONICAL_DOMAIN_EXEMPT=[r"^api.example.com$"])
def test_exceptions(self):
response = self.client.get("/", headers={"host": "api.example.com"})
self.assertEqual(response.status_code, 200)
Expand All @@ -53,6 +53,20 @@ def test_exceptions(self):
self.assertEqual(response.status_code, 301)
self.assertEqual(response["Location"], "http://example.com/")

@override_settings(SECURE_REDIRECT_EXEMPT=[r'^no-ssl$'])
def test_path_exceptions(self):
response = self.client.get("/no-ssl", headers={"host": "example.org"})
self.assertEqual(response.status_code, 301)
self.assertEqual(response["Location"], "http://example.com/no-ssl")

response = self.client.get("/", headers={"host": "example.org"}, secure=True)
self.assertEqual(response.status_code, 301)
self.assertEqual(response["Location"], "https://example.com/")

response = self.client.get("/no-ssl", headers={"host": "example.com"}, secure=True)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b"Hello world")


@override_settings(
MIDDLEWARE=["canonical_domain.middleware.canonical_domain"],
Expand All @@ -79,7 +93,7 @@ def test_match(self):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b"Hello world")

@override_settings(CANONICAL_DOMAIN_EXCEPTIONS=["api.example.com"])
@override_settings(CANONICAL_DOMAIN_EXEMPT=["^api.example.com$"])
def test_exceptions(self):
response = self.client.get("/", headers={"host": "api.example.com"}, secure=True)
self.assertEqual(response.status_code, 200)
Expand All @@ -97,6 +111,28 @@ def test_exceptions(self):
self.assertEqual(response.status_code, 301)
self.assertEqual(response["Location"], "https://example.com/")

@override_settings(SECURE_REDIRECT_EXEMPT=[r'^no-ssl$'])
def test_path_exceptions(self):
# Unsecure path matching exempt and ssl_host should remain
response = self.client.get("/no-ssl", headers={"host": "example.com"})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b"Hello world")

# If the path doesn't match ssl_host we redirect, but don't change to https
response = self.client.get("/no-ssl", headers={"host": "example.org"})
self.assertEqual(response.status_code, 301)
self.assertEqual(response["Location"], "http://example.com/no-ssl")

# We change to https on the path that doesn't match the exempt
response = self.client.get("/", headers={"host": "example.org"})
self.assertEqual(response.status_code, 301)
self.assertEqual(response["Location"], "https://example.com/")

# Https version should also work
response = self.client.get("/no-ssl", headers={"host": "example.com"}, secure=True)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.content, b"Hello world")


class ChecksTestCase(TestCase):
def assertCheckCodes(self, check_results, codes):
Expand Down
5 changes: 4 additions & 1 deletion tests/testapp/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@
from django.urls import path


urlpatterns = [path("", lambda request: HttpResponse("Hello world"))]
urlpatterns = [
path("", lambda request: HttpResponse("Hello world")),
path("no-ssl", lambda request: HttpResponse("Hello world")),
]

0 comments on commit ff7266a

Please sign in to comment.