Skip to content

Commit 58e1c2a

Browse files
authored
Merge commit from fork
1 parent a1125f3 commit 58e1c2a

File tree

3 files changed

+60
-39
lines changed

3 files changed

+60
-39
lines changed

readthedocs/projects/forms.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,20 +1012,11 @@ def clean_domain(self):
10121012
if not parsed.scheme:
10131013
parsed = self._safe_urlparse(f"https://{domain}")
10141014

1015-
if not parsed.netloc:
1015+
domain_string = parsed.netloc.strip()
1016+
if not domain_string:
10161017
raise forms.ValidationError(f"{domain} is not a valid domain.")
10171018

1018-
domain_string = parsed.netloc
1019-
1020-
# Don't allow internal domains to be added, we have:
1021-
# - Dashboard domain
1022-
# - Public domain (from where documentation pages are served)
1023-
# - External version domain (from where PR previews are served)
1024-
for invalid_domain in [
1025-
settings.PRODUCTION_DOMAIN,
1026-
settings.PUBLIC_DOMAIN,
1027-
settings.RTD_EXTERNAL_VERSION_DOMAIN,
1028-
]:
1019+
for invalid_domain in settings.RTD_RESTRICTED_DOMAINS:
10291020
if invalid_domain and domain_string.endswith(invalid_domain):
10301021
raise forms.ValidationError(f"{invalid_domain} is not a valid domain.")
10311022

readthedocs/rtd_tests/tests/test_domains.py

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -81,39 +81,39 @@ def test_production_domain_not_allowed(self):
8181
f"{settings.PRODUCTION_DOMAIN} is not a valid domain.",
8282
)
8383

84-
@override_settings(PUBLIC_DOMAIN="readthedocs.io")
85-
def test_public_domain_not_allowed(self):
84+
@override_settings(
85+
RTD_RESTRICTED_DOMAINS=[
86+
"readthedocs.org",
87+
"readthedocs.io",
88+
"readthedocs.build",
89+
],
90+
)
91+
def test_restricted_domains_not_allowed(self):
8692
"""Make sure user can not enter public domain name."""
87-
form = DomainForm(
88-
{"domain": settings.PUBLIC_DOMAIN},
89-
project=self.project,
90-
)
91-
self.assertFalse(form.is_valid())
92-
self.assertEqual(
93-
form.errors["domain"][0], f"{settings.PUBLIC_DOMAIN} is not a valid domain."
94-
)
95-
96-
form2 = DomainForm(
97-
{"domain": "docs." + settings.PUBLIC_DOMAIN},
98-
project=self.project,
99-
)
100-
self.assertFalse(form2.is_valid())
101-
self.assertEqual(
102-
form2.errors["domain"][0],
103-
f"{settings.PUBLIC_DOMAIN} is not a valid domain.",
104-
)
93+
invalid_domains = [
94+
"readthedocs.org",
95+
"test.readthedocs.org",
96+
"app.readthedocs.org",
97+
"test.app.readthedocs.org",
98+
"readthedocs.io",
99+
"test.readthedocs.io",
100+
"docs.readthedocs.io",
101+
"test.docs.readthedocs.io",
102+
"readthedocs.build",
103+
"test.readthedocs.build",
104+
"docs.readthedocs.build",
105+
"test.docs.readthedocs.build",
106+
# Trailing white spaces, sneaky.
107+
"https:// readthedocs.org /",
108+
]
105109

106-
@override_settings(RTD_EXTERNAL_VERSION_DOMAIN="readthedocs.build")
107-
def test_external_domain_not_allowed(self):
108-
for domain in ["readthedocs.build", "test.readthedocs.build"]:
110+
for domain in invalid_domains:
109111
form = DomainForm(
110112
{"domain": domain},
111113
project=self.project,
112114
)
113-
self.assertFalse(form.is_valid())
114-
self.assertEqual(
115-
form.errors["domain"][0], "readthedocs.build is not a valid domain."
116-
)
115+
assert not form.is_valid(), domain
116+
assert "is not a valid domain." in form.errors["domain"][0]
117117

118118
def test_domain_with_path(self):
119119
form = DomainForm(

readthedocs/settings/base.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,36 @@ def SHOW_DEBUG_TOOLBAR(self):
9898
RTD_INTERSPHINX_URL = "https://{}".format(PRODUCTION_DOMAIN)
9999
RTD_EXTERNAL_VERSION_DOMAIN = "external-builds.readthedocs.io"
100100

101+
@property
102+
def RTD_RESTRICTED_DOMAINS(self):
103+
"""
104+
Domains that are restricted for users to use as custom domains.
105+
106+
This is to avoid users hijacking our domains.
107+
We return the last two parts of our public domains to cover all subdomains,
108+
e.g, if our domain is "app.readthedocs.org", we restrict all subdomains from "readthedocs.org".
109+
110+
If your domain is like "readthedocs.co.uk", you might want to override this property.
111+
112+
We recommend disallowing:
113+
114+
- Dashboard domain
115+
- Public domain (from where documentation pages are served)
116+
- External version domain (from where PR previews are served)
117+
- Any public domains that point to the validation record (e.g., CNAME to readthedocs.io)
118+
"""
119+
domains = [
120+
self.PRODUCTION_DOMAIN,
121+
self.PUBLIC_DOMAIN,
122+
self.RTD_EXTERNAL_VERSION_DOMAIN,
123+
"rtfd.io",
124+
"rtfd.org",
125+
]
126+
return [
127+
".".join(domain.split(".")[-2:])
128+
for domain in domains
129+
]
130+
101131
# Doc Builder Backends
102132
MKDOCS_BACKEND = "readthedocs.doc_builder.backends.mkdocs"
103133
SPHINX_BACKEND = "readthedocs.doc_builder.backends.sphinx"

0 commit comments

Comments
 (0)