Skip to content

Proxito: add CORS headers only on public versions #10737

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

Merged
merged 5 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 2 additions & 0 deletions dockerfiles/nginx/proxito.conf.template
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ server {
add_header Access-Control-Allow-Origin $access_control_allow_origin always;
set $access_control_allow_headers $upstream_http_access_control_allow_headers;
add_header Access-Control-Allow-Headers $access_control_allow_headers always;
set $access_control_allow_methods $upstream_http_access_control_allow_methods;
add_header Access-Control-Allow-Methods $access_control_allow_methods always;
set $x_frame_options $upstream_http_x_frame_options;
add_header X-Frame-Options $x_frame_options always;
set $x_content_type_options $upstream_http_x_content_type_options;
Expand Down
28 changes: 28 additions & 0 deletions readthedocs/proxito/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
unresolver,
)
from readthedocs.core.utils import get_cache_tag
from readthedocs.projects.constants import PUBLIC
from readthedocs.projects.models import Project
from readthedocs.proxito.cache import add_cache_tags, cache_response, private_response
from readthedocs.proxito.redirects import redirect_to_https
Expand Down Expand Up @@ -305,6 +306,32 @@ def add_hosting_integrations_headers(self, request, response):
if addons:
response["X-RTD-Hosting-Integrations"] = "true"

def add_cors_headers(self, request, response):
"""
Add CORS headers only on PUBLIC versions.

DocDiff addons requires making a request from
``RTD_EXTERNAL_VERSION_DOMAIN`` to ``PUBLIC_DOMAIN`` to be able to
compare both DOMs and show the visual differences.

This request needs ``Access-Control-Allow-Origin`` HTTP headers to be
accepted by browsers. However, we cannot expose these headers for
documentation that's not PUBLIC.
"""
project_slug = getattr(request, "path_project_slug", "")
version_slug = getattr(request, "path_version_slug", "")

if project_slug and version_slug:
allow_cors = Version.objects.filter(
project__slug=project_slug,
slug=version_slug,
privacy_level=PUBLIC,
).exists()
if allow_cors:
response.headers["Access-Control-Allow-Origin"] = "*.readthedocs.build"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ericholscher I don't think you can use a wildcard within the origin, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin#directives.

Only a single origin can be specified. If the server supports clients from multiple origins, it must return the origin for the specific client making the request.

This type of wildcard needs to be implemented in the server, something like:

if is_valid_origing(origin):
    response.headers["Access-Control-Allow-Origin"] = origin

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having this to * is fine for .org (github and gitlab pages have it to this value, for public pages at least). Maybe we can just start by allowing CORS on .org only.

response.headers["Access-Control-Allow-Methods"] = "OPTIONS, GET"
return response

def _get_https_redirect(self, request):
"""
Get a redirect response if the request should be redirected to HTTPS.
Expand Down Expand Up @@ -342,4 +369,5 @@ def process_response(self, request, response): # noqa
self.add_hsts_headers(request, response)
self.add_user_headers(request, response)
self.add_hosting_integrations_headers(request, response)
self.add_cors_headers(request, response)
return response
25 changes: 25 additions & 0 deletions readthedocs/proxito/tests/test_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from django.urls import reverse

from readthedocs.builds.constants import LATEST
from readthedocs.projects.constants import PRIVATE, PUBLIC
from readthedocs.projects.models import Domain, HTTPHeader

from .base import BaseDocServing
Expand Down Expand Up @@ -158,6 +159,30 @@ def test_hosting_integrations_header(self):
self.assertIsNotNone(r.get("X-RTD-Hosting-Integrations"))
self.assertEqual(r["X-RTD-Hosting-Integrations"], "true")

def test_cors_headers_private_version(self):
version = self.project.versions.get(slug=LATEST)
version.privacy_level = PRIVATE
version.save()

r = self.client.get(
"/en/latest/", secure=True, headers={"host": "project.dev.readthedocs.io"}
)
self.assertEqual(r.status_code, 200)
self.assertIsNone(r.get("Access-Control-Allow-Origin"))
self.assertIsNone(r.get("Access-Control-Allow-Methods"))

def test_cors_headers_public_version(self):
version = self.project.versions.get(slug=LATEST)
version.privacy_level = PUBLIC
version.save()

r = self.client.get(
"/en/latest/", secure=True, headers={"host": "project.dev.readthedocs.io"}
)
self.assertEqual(r.status_code, 200)
self.assertEqual(r["Access-Control-Allow-Origin"], "*")
self.assertEqual(r["Access-Control-Allow-Methods"], "OPTIONS, GET")

@override_settings(ALLOW_PRIVATE_REPOS=False)
def test_cache_headers_public_version_with_private_projects_not_allowed(self):
r = self.client.get(
Expand Down