Skip to content

Commit 399cedc

Browse files
authored
Merge pull request #1338 from makeabilitylab/1329-verify-sitemap-scheme
Drop sitemap https pin; verify SECURE_PROXY_SSL_HEADER via request.scheme (#1329)
2 parents 116fd79 + 7d84537 commit 399cedc

2 files changed

Lines changed: 39 additions & 25 deletions

File tree

website/sitemaps.py

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@
1111
``makeabilitylab.cs.washington.edu`` in prod, ``makeabilitylab-test...`` on the
1212
test server, and ``localhost`` in dev — no per-environment configuration.
1313
14+
Scheme handling: the ``<loc>`` protocol comes from ``request.scheme``. Behind
15+
UW CSE's TLS-terminating Apache proxy that is now ``https`` because
16+
``SECURE_PROXY_SSL_HEADER`` trusts the proxy's ``X-Forwarded-Proto`` header
17+
(#1329); in local dev it is ``http``. We previously pinned ``protocol="https"``
18+
to paper over Django seeing ``http`` behind the proxy — that workaround is gone
19+
now that the scheme is correct at the framework level.
20+
1421
We map only the pages that have real, indexable URLs:
1522
- static listing pages (home, people, publications, projects, awards, news)
1623
- one entry per visible Project -> /project/<short_name>/
@@ -37,22 +44,7 @@ def _latest(model, field):
3744
)
3845

3946

40-
class _HttpsSitemap(Sitemap):
41-
"""
42-
Base sitemap that pins generated URLs to the https scheme.
43-
44-
Apache terminates TLS and proxies to Django over plain HTTP, so the
45-
request scheme Django sees is ``http``. Without this, RequestSite would
46-
emit ``http://`` <loc> URLs that only 302-redirect to https — making the
47-
sitemap advertise non-canonical URLs with an extra hop. Pinning the
48-
protocol here makes every sitemap list the canonical https URLs directly.
49-
(Cosmetic only in local dev, where the site is served over http.)
50-
"""
51-
52-
protocol = "https"
53-
54-
55-
class StaticViewSitemap(_HttpsSitemap):
47+
class StaticViewSitemap(Sitemap):
5648
"""Top-level listing pages that aren't tied to a single model instance."""
5749

5850
changefreq = "weekly"
@@ -106,7 +98,7 @@ def lastmod(self, item):
10698
return None
10799

108100

109-
class ProjectSitemap(_HttpsSitemap):
101+
class ProjectSitemap(Sitemap):
110102
"""Public project pages: /project/<short_name>/."""
111103

112104
changefreq = "weekly"
@@ -127,7 +119,7 @@ def lastmod(self, obj):
127119
return obj.updated
128120

129121

130-
class PersonSitemap(_HttpsSitemap):
122+
class PersonSitemap(Sitemap):
131123
"""Public people pages: /member/<url_name>/."""
132124

133125
changefreq = "monthly"
@@ -153,7 +145,7 @@ def lastmod(self, obj):
153145
return obj.bio_datetime_modified
154146

155147

156-
class NewsSitemap(_HttpsSitemap):
148+
class NewsSitemap(Sitemap):
157149
"""News item pages: /news/<slug>/."""
158150

159151
changefreq = "monthly"

website/tests/test_sitemap.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import re
1212
from datetime import date
1313

14+
from django.test import override_settings
15+
1416
from website.tests.base import DatabaseTestCase
1517

1618

@@ -73,16 +75,36 @@ def test_static_listing_pages_have_lastmod(self):
7375
self.assertTrue(listing, "expected a /news/ listing entry in the sitemap")
7476
self.assertIn("<lastmod>", listing[0])
7577

76-
def test_sitemap_uses_https_scheme(self):
77-
# Apache proxies to Django over plain HTTP, so without a pinned
78-
# protocol the <loc> URLs would be http:// and only 302-redirect to
79-
# https. Every <loc> must be canonical https. See _HttpsSitemap.
78+
@override_settings(SECURE_PROXY_SSL_HEADER=("HTTP_X_FORWARDED_PROTO", "https"))
79+
def test_sitemap_honors_forwarded_proto_header(self):
80+
# The sitemap scheme follows request.scheme (we no longer pin
81+
# protocol="https"). Behind UW CSE's TLS-terminating proxy, Django
82+
# reaches https via SECURE_PROXY_SSL_HEADER trusting X-Forwarded-Proto
83+
# (#1329). Simulate that header here and confirm every <loc> is https.
8084
self.make_project(name="Scheme Proj", short_name="schemeproj",
8185
is_visible=True)
82-
body = self.client.get("/sitemap.xml").content.decode()
86+
body = self.client.get(
87+
"/sitemap.xml", HTTP_X_FORWARDED_PROTO="https"
88+
).content.decode()
8389
locs = re.findall(r"<loc>(.*?)</loc>", body)
8490
self.assertTrue(locs) # guard against an empty sitemap passing vacuously
8591
self.assertFalse(
8692
[loc for loc in locs if not loc.startswith("https://")],
87-
"all sitemap <loc> URLs should use the https scheme",
93+
"with X-Forwarded-Proto=https the sitemap <loc> URLs should be https",
94+
)
95+
96+
def test_sitemap_scheme_follows_request(self):
97+
# With the protocol pin removed, a plain request (no forwarded-proto,
98+
# no SECURE_PROXY_SSL_HEADER) reflects the request scheme — http here.
99+
# This is the local-dev / direct-request case; the proxy supplies https
100+
# in the deployed environments (see the test above and #1329).
101+
self.make_project(name="Plain Proj", short_name="plainproj",
102+
is_visible=True)
103+
body = self.client.get("/sitemap.xml").content.decode()
104+
locs = re.findall(r"<loc>(.*?)</loc>", body)
105+
self.assertTrue(locs)
106+
self.assertTrue(
107+
all(loc.startswith("http://") for loc in locs),
108+
"without a forwarded-proto header the sitemap should reflect the "
109+
"request scheme (http) rather than a pinned https",
88110
)

0 commit comments

Comments
 (0)