Skip to content

Commit 810abd3

Browse files
committed
Proxito: fix https and canonical redirects
When doing a https redirect we were adding the default language and version (/en/latest/) if the current url didn't have those components. And similar when doing a redirect to a canonical domain. Fix #8183
1 parent 926a6e8 commit 810abd3

File tree

4 files changed

+85
-18
lines changed

4 files changed

+85
-18
lines changed

readthedocs/proxito/constants.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
REDIRECT_HTTPS = 'https'
2+
REDIRECT_CANONICAL_CNAME = 'canonical-cname'
3+
REDIRECT_SUBPROJECT_MAIN_DOMAIN = 'subproject-main-domain'

readthedocs/proxito/middleware.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from django.utils.deprecation import MiddlewareMixin
1717

1818
from readthedocs.projects.models import Domain, Project, ProjectRelationship
19+
from readthedocs.proxito import constants
1920

2021
log = logging.getLogger(__name__) # noqa
2122

@@ -65,13 +66,13 @@ def map_host_to_project_slug(request): # pylint: disable=too-many-return-statem
6566
https=True,
6667
).exists():
6768
log.debug('Proxito Public Domain -> Canonical Domain Redirect: host=%s', host)
68-
request.canonicalize = 'canonical-cname'
69+
request.canonicalize = constants.REDIRECT_CANONICAL_CNAME
6970
elif (
7071
ProjectRelationship.objects.
7172
filter(child__slug=project_slug).exists()
7273
):
7374
log.debug('Proxito Public Domain -> Subproject Main Domain Redirect: host=%s', host)
74-
request.canonicalize = 'subproject-main-domain'
75+
request.canonicalize = constants.REDIRECT_SUBPROJECT_MAIN_DOMAIN
7576
return project_slug
7677

7778
# TODO: This can catch some possibly valid domains (docs.readthedocs.io.com) for example
@@ -107,7 +108,7 @@ def map_host_to_project_slug(request): # pylint: disable=too-many-return-statem
107108
if domain.https and not request.is_secure():
108109
# Redirect HTTP -> HTTPS (302) for this custom domain
109110
log.debug('Proxito CNAME HTTPS Redirect: host=%s', host)
110-
request.canonicalize = 'https'
111+
request.canonicalize = constants.REDIRECT_HTTPS
111112

112113
# NOTE: consider redirecting non-canonical custom domains to the canonical one
113114
# Whether that is another custom domain or the public domain

readthedocs/proxito/tests/test_redirects.py

+40-2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,28 @@ def test_root_url(self):
2626
r['Location'], 'https://project.dev.readthedocs.io/en/latest/',
2727
)
2828

29+
def test_custom_domain_root_url(self):
30+
self.domain.canonical = True
31+
self.domain.save()
32+
33+
r = self.client.get('/', HTTP_HOST=self.domain.domain, secure=True)
34+
self.assertEqual(r.status_code, 302)
35+
self.assertEqual(
36+
r['Location'], f'https://{self.domain.domain}/en/latest/',
37+
)
38+
self.assertEqual(r['X-RTD-Redirect'], 'system')
39+
40+
def test_custom_domain_root_url_no_slash(self):
41+
self.domain.canonical = True
42+
self.domain.save()
43+
44+
r = self.client.get('', HTTP_HOST=self.domain.domain, secure=True)
45+
self.assertEqual(r.status_code, 302)
46+
self.assertEqual(
47+
r['Location'], f'https://{self.domain.domain}/en/latest/',
48+
)
49+
self.assertEqual(r['X-RTD-Redirect'], 'system')
50+
2951
def test_single_version_root_url_doesnt_redirect(self):
3052
self.project.single_version = True
3153
self.project.save()
@@ -121,7 +143,7 @@ def test_canonicalize_https_redirect(self):
121143
r = self.client.get('/', HTTP_HOST=self.domain.domain)
122144
self.assertEqual(r.status_code, 302)
123145
self.assertEqual(
124-
r['Location'], f'https://{self.domain.domain}/en/latest/',
146+
r['Location'], f'https://{self.domain.domain}/',
125147
)
126148
self.assertEqual(r['X-RTD-Redirect'], 'https')
127149

@@ -141,7 +163,7 @@ def test_canonicalize_public_domain_to_cname_redirect(self):
141163
r = self.client.get('/', HTTP_HOST='project.dev.readthedocs.io')
142164
self.assertEqual(r.status_code, 302)
143165
self.assertEqual(
144-
r['Location'], f'https://{self.domain.domain}/en/latest/',
166+
r['Location'], f'https://{self.domain.domain}/',
145167
)
146168
self.assertEqual(r['X-RTD-Redirect'], 'canonical-cname')
147169

@@ -153,6 +175,22 @@ def test_canonicalize_public_domain_to_cname_redirect(self):
153175
)
154176
self.assertEqual(r['X-RTD-Redirect'], 'canonical-cname')
155177

178+
def test_translation_redirect(self):
179+
r = self.client.get('/', HTTP_HOST='translation.dev.readthedocs.io')
180+
self.assertEqual(r.status_code, 302)
181+
self.assertEqual(
182+
r['Location'], f'https://project.dev.readthedocs.io/es/latest/',
183+
)
184+
self.assertEqual(r['X-RTD-Redirect'], 'system')
185+
186+
def test_translation_secure_redirect(self):
187+
r = self.client.get('/', HTTP_HOST='translation.dev.readthedocs.io', secure=True)
188+
self.assertEqual(r.status_code, 302)
189+
self.assertEqual(
190+
r['Location'], f'https://project.dev.readthedocs.io/es/latest/',
191+
)
192+
self.assertEqual(r['X-RTD-Redirect'], 'system')
193+
156194
# We are not canonicalizing custom domains -> public domain for now
157195
@pytest.mark.xfail(strict=True)
158196
def test_canonicalize_cname_to_public_domain_redirect(self):

readthedocs/proxito/views/mixins.py

+38-13
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,24 @@
22
import logging
33
import mimetypes
44
from urllib.parse import urlparse, urlunparse
5-
from slugify import slugify as unicode_slugify
65

76
from django.conf import settings
87
from django.http import (
98
HttpResponse,
10-
HttpResponseRedirect,
119
HttpResponsePermanentRedirect,
10+
HttpResponseRedirect,
1211
)
1312
from django.shortcuts import render
1413
from django.utils.encoding import iri_to_uri
1514
from django.views.static import serve
15+
from slugify import slugify as unicode_slugify
1616

1717
from readthedocs.builds.constants import EXTERNAL, INTERNAL
1818
from readthedocs.core.resolver import resolve
19+
from readthedocs.proxito.constants import (
20+
REDIRECT_CANONICAL_CNAME,
21+
REDIRECT_HTTPS,
22+
)
1923
from readthedocs.redirects.exceptions import InfiniteRedirectException
2024
from readthedocs.storage import build_media_storage
2125

@@ -169,19 +173,40 @@ def canonical_redirect(self, request, final_project, version_slug, filename):
169173
"""
170174
Return a redirect to the canonical domain including scheme.
171175
172-
This is normally used HTTP -> HTTPS redirects or redirects to/from custom domains.
176+
The following cases are covered:
177+
178+
- Redirect a custom from http to https (if supported)
179+
http://project.rtd.io/ -> https://project.rtd.io/
180+
- Redirect a domain to a canonical domain (http or https).
181+
http://project.rtd.io/ -> https://docs.test.com/
182+
http://project.rtd.io/foo/bar/ -> https://docs.test.com/foo/bar/
183+
- Redirect from a subproject domain to the main domain
184+
https://subproject.rtd.io/en/latest/foo -> https://main.rtd.io/projects/subproject/en/latest/foo # noqa
185+
https://subproject.rtd.io/en/latest/foo -> https://docs.test.com/projects/subproject/en/latest/foo # noqa
173186
"""
174-
full_path = request.get_full_path()
175-
urlparse_result = urlparse(full_path)
176-
to = resolve(
177-
project=final_project,
178-
version_slug=version_slug,
179-
filename=filename,
180-
query_params=urlparse_result.query,
181-
external=hasattr(request, 'external_domain'),
182-
)
187+
from_url = request.build_absolute_uri()
188+
parsed_from = urlparse(from_url)
183189

184-
if full_path == to:
190+
redirect_type = getattr(request, 'canonicalize', None)
191+
if redirect_type == REDIRECT_HTTPS:
192+
to = parsed_from._replace(scheme='https').geturl()
193+
else:
194+
to = resolve(
195+
project=final_project,
196+
version_slug=version_slug,
197+
filename=filename,
198+
query_params=parsed_from.query,
199+
external=hasattr(request, 'external_domain'),
200+
)
201+
# When a canonical redirect is done, only change the domain.
202+
if redirect_type == REDIRECT_CANONICAL_CNAME:
203+
to_parsed = urlparse(to)
204+
to = parsed_from._replace(
205+
scheme=to_parsed.scheme,
206+
netloc=to_parsed.netloc,
207+
).geturl()
208+
209+
if from_url == to:
185210
# check that we do have a response and avoid infinite redirect
186211
log.warning(
187212
'Infinite Redirect: FROM URL is the same than TO URL. url=%s',

0 commit comments

Comments
 (0)