From d7e4d51c7c2eaf3bc62a2d914c277aed87a398bd Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Tue, 19 Sep 2023 10:45:09 +0200 Subject: [PATCH 1/6] Proxito: update CORS settings - Only add CORS headers on community site - Explicit host on `Access-Control-Allow-Origin` - Only query the database if the host ends with `RTD_EXTERNAL_VERSION_DOMAIN` - Add more tests Continuation of #10737 --- readthedocs/proxito/middleware.py | 16 ++++- readthedocs/proxito/tests/test_headers.py | 75 ++++++++++++++++++++--- 2 files changed, 79 insertions(+), 12 deletions(-) diff --git a/readthedocs/proxito/middleware.py b/readthedocs/proxito/middleware.py index 667f5ea5391..64cd77676be 100644 --- a/readthedocs/proxito/middleware.py +++ b/readthedocs/proxito/middleware.py @@ -318,18 +318,30 @@ def add_cors_headers(self, request, response): accepted by browsers. However, we cannot expose these headers for documentation that's not PUBLIC. """ + + # Disable CORS on "Read the Docs for Business" for now. + # We want to be pretty sure this logic is OK before enabling it there. + if settings.ALLOW_PRIVATE_REPOS: + return + project_slug = getattr(request, "path_project_slug", "") version_slug = getattr(request, "path_version_slug", "") + host = request.get_host() - if project_slug and version_slug: + if ( + project_slug + and version_slug + and host.endswith(settings.RTD_EXTERNAL_VERSION_DOMAIN) + ): 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" + response.headers["Access-Control-Allow-Origin"] = host response.headers["Access-Control-Allow-Methods"] = "OPTIONS, GET" + return response def _get_https_redirect(self, request): diff --git a/readthedocs/proxito/tests/test_headers.py b/readthedocs/proxito/tests/test_headers.py index 9f619c5655a..bd9429db749 100644 --- a/readthedocs/proxito/tests/test_headers.py +++ b/readthedocs/proxito/tests/test_headers.py @@ -2,7 +2,8 @@ from django.test import override_settings from django.urls import reverse -from readthedocs.builds.constants import LATEST +from readthedocs.builds.constants import EXTERNAL, LATEST +from readthedocs.builds.models import Version from readthedocs.projects.constants import PRIVATE, PUBLIC from readthedocs.projects.models import Domain, HTTPHeader @@ -12,6 +13,7 @@ @override_settings( PUBLIC_DOMAIN="dev.readthedocs.io", PUBLIC_DOMAIN_USES_HTTPS=True, + RTD_EXTERNAL_VERSION_DOMAIN="dev.readthedocs.build", ) class ProxitoHeaderTests(BaseDocServing): def test_redirect_headers(self): @@ -159,10 +161,15 @@ 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() + def test_cors_headers_non_external_domain(self): + fixture.get( + Version, + project=self.project, + slug="111", + active=True, + privacy_level=PUBLIC, + type=EXTERNAL, + ) r = self.client.get( "/en/latest/", secure=True, headers={"host": "project.dev.readthedocs.io"} @@ -171,18 +178,66 @@ def test_cors_headers_private_version(self): self.assertIsNone(r.get("Access-Control-Allow-Origin")) self.assertIsNone(r.get("Access-Control-Allow-Methods")) + def test_cors_headers_private_version(self): + fixture.get( + Version, + project=self.project, + slug="111", + active=True, + privacy_level=PRIVATE, + type=EXTERNAL, + ) + + r = self.client.get( + "/en/111/", + secure=True, + headers={"host": "project--111.dev.readthedocs.build"}, + ) + 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() + fixture.get( + Version, + project=self.project, + slug="111", + active=True, + privacy_level=PUBLIC, + type=EXTERNAL, + ) r = self.client.get( - "/en/latest/", secure=True, headers={"host": "project.dev.readthedocs.io"} + "/en/111/", + secure=True, + headers={"host": "project--111.dev.readthedocs.build"}, ) self.assertEqual(r.status_code, 200) - self.assertEqual(r["Access-Control-Allow-Origin"], "*.readthedocs.build") + self.assertEqual( + r["Access-Control-Allow-Origin"], "project--111.dev.readthedocs.build" + ) self.assertEqual(r["Access-Control-Allow-Methods"], "OPTIONS, GET") + @override_settings(ALLOW_PRIVATE_REPOS=True) + def test_cors_headers_public_version_allow_private_repositories(self): + fixture.get( + Version, + project=self.project, + slug="111", + active=True, + privacy_level=PUBLIC, + type=EXTERNAL, + ) + + r = self.client.get( + "/en/111/", + secure=True, + headers={"host": "project--111.dev.readthedocs.build"}, + ) + self.assertEqual(r.status_code, 200) + self.assertIsNone(r.get("Access-Control-Allow-Origin")) + self.assertIsNone(r.get("Access-Control-Allow-Methods")) + @override_settings(ALLOW_PRIVATE_REPOS=False) def test_cache_headers_public_version_with_private_projects_not_allowed(self): r = self.client.get( From e20382a5fe21acd1cd1700146f0b7a207955925d Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Tue, 19 Sep 2023 17:48:02 +0200 Subject: [PATCH 2/6] Add `Vary: Origin` header --- readthedocs/proxito/middleware.py | 1 + readthedocs/proxito/tests/test_headers.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/readthedocs/proxito/middleware.py b/readthedocs/proxito/middleware.py index 64cd77676be..4b04cbff530 100644 --- a/readthedocs/proxito/middleware.py +++ b/readthedocs/proxito/middleware.py @@ -341,6 +341,7 @@ def add_cors_headers(self, request, response): if allow_cors: response.headers["Access-Control-Allow-Origin"] = host response.headers["Access-Control-Allow-Methods"] = "OPTIONS, GET" + response.headers["Vary"] = "Origin" return response diff --git a/readthedocs/proxito/tests/test_headers.py b/readthedocs/proxito/tests/test_headers.py index bd9429db749..5c43fcd5c5c 100644 --- a/readthedocs/proxito/tests/test_headers.py +++ b/readthedocs/proxito/tests/test_headers.py @@ -196,6 +196,7 @@ def test_cors_headers_private_version(self): self.assertEqual(r.status_code, 200) self.assertIsNone(r.get("Access-Control-Allow-Origin")) self.assertIsNone(r.get("Access-Control-Allow-Methods")) + self.assertIsNone(r.get("Vary")) def test_cors_headers_public_version(self): fixture.get( @@ -217,6 +218,7 @@ def test_cors_headers_public_version(self): r["Access-Control-Allow-Origin"], "project--111.dev.readthedocs.build" ) self.assertEqual(r["Access-Control-Allow-Methods"], "OPTIONS, GET") + self.assertEqual(r["Vary"], "Origin") @override_settings(ALLOW_PRIVATE_REPOS=True) def test_cors_headers_public_version_allow_private_repositories(self): From a694ea847650fc96ff80b2e06481010758ac9f0d Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Tue, 19 Sep 2023 19:07:09 +0200 Subject: [PATCH 3/6] Use Django internals to patch `Vary` header. --- readthedocs/proxito/middleware.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/readthedocs/proxito/middleware.py b/readthedocs/proxito/middleware.py index 4b04cbff530..62e6262cb8d 100644 --- a/readthedocs/proxito/middleware.py +++ b/readthedocs/proxito/middleware.py @@ -13,6 +13,7 @@ from django.core.exceptions import SuspiciousOperation from django.shortcuts import redirect from django.urls import reverse +from django.utils.cache import patch_vary_headers from django.utils.deprecation import MiddlewareMixin from readthedocs.builds.models import Version @@ -341,7 +342,7 @@ def add_cors_headers(self, request, response): if allow_cors: response.headers["Access-Control-Allow-Origin"] = host response.headers["Access-Control-Allow-Methods"] = "OPTIONS, GET" - response.headers["Vary"] = "Origin" + patch_vary_headers(response, ("origin",)) return response From 3e733e836d16af08f6779bf0f44d870b588fd5fa Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Tue, 19 Sep 2023 19:10:52 +0200 Subject: [PATCH 4/6] Use the `Origin` header from request to check for the allowed domain --- readthedocs/proxito/middleware.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/readthedocs/proxito/middleware.py b/readthedocs/proxito/middleware.py index 62e6262cb8d..dc9474fd5fd 100644 --- a/readthedocs/proxito/middleware.py +++ b/readthedocs/proxito/middleware.py @@ -327,12 +327,12 @@ def add_cors_headers(self, request, response): project_slug = getattr(request, "path_project_slug", "") version_slug = getattr(request, "path_version_slug", "") - host = request.get_host() + origin = request.headers.get("origin") if ( project_slug and version_slug - and host.endswith(settings.RTD_EXTERNAL_VERSION_DOMAIN) + and origin.endswith(settings.RTD_EXTERNAL_VERSION_DOMAIN) ): allow_cors = Version.objects.filter( project__slug=project_slug, @@ -340,7 +340,7 @@ def add_cors_headers(self, request, response): privacy_level=PUBLIC, ).exists() if allow_cors: - response.headers["Access-Control-Allow-Origin"] = host + response.headers["Access-Control-Allow-Origin"] = origin response.headers["Access-Control-Allow-Methods"] = "OPTIONS, GET" patch_vary_headers(response, ("origin",)) From af4684a38ea6fcdaf70ce34a16d6945ae21f8c89 Mon Sep 17 00:00:00 2001 From: Manuel Kaufmann Date: Tue, 19 Sep 2023 19:13:33 +0200 Subject: [PATCH 5/6] Update tests to use `origin` header --- readthedocs/proxito/tests/test_headers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/readthedocs/proxito/tests/test_headers.py b/readthedocs/proxito/tests/test_headers.py index 5c43fcd5c5c..491af180ffd 100644 --- a/readthedocs/proxito/tests/test_headers.py +++ b/readthedocs/proxito/tests/test_headers.py @@ -191,7 +191,7 @@ def test_cors_headers_private_version(self): r = self.client.get( "/en/111/", secure=True, - headers={"host": "project--111.dev.readthedocs.build"}, + headers={"origin": "project--111.dev.readthedocs.build"}, ) self.assertEqual(r.status_code, 200) self.assertIsNone(r.get("Access-Control-Allow-Origin")) @@ -211,7 +211,7 @@ def test_cors_headers_public_version(self): r = self.client.get( "/en/111/", secure=True, - headers={"host": "project--111.dev.readthedocs.build"}, + headers={"origin": "project--111.dev.readthedocs.build"}, ) self.assertEqual(r.status_code, 200) self.assertEqual( @@ -234,7 +234,7 @@ def test_cors_headers_public_version_allow_private_repositories(self): r = self.client.get( "/en/111/", secure=True, - headers={"host": "project--111.dev.readthedocs.build"}, + headers={"origin": "project--111.dev.readthedocs.build"}, ) self.assertEqual(r.status_code, 200) self.assertIsNone(r.get("Access-Control-Allow-Origin")) From ec23f9beb8c1e12fa50c52ef736f265f21aa19b5 Mon Sep 17 00:00:00 2001 From: Santos Gallegos Date: Tue, 19 Sep 2023 18:03:59 -0500 Subject: [PATCH 6/6] Allow cross-origin requests for public versions' docs --- readthedocs/embed/v3/tests/test_access.py | 5 + readthedocs/proxito/middleware.py | 29 +++-- readthedocs/proxito/tests/test_headers.py | 114 +++++++++++-------- readthedocs/search/tests/test_proxied_api.py | 2 + 4 files changed, 92 insertions(+), 58 deletions(-) diff --git a/readthedocs/embed/v3/tests/test_access.py b/readthedocs/embed/v3/tests/test_access.py index 955916c36e9..0b6a17d0280 100644 --- a/readthedocs/embed/v3/tests/test_access.py +++ b/readthedocs/embed/v3/tests/test_access.py @@ -2,6 +2,7 @@ from unittest import mock import pytest +from corsheaders.middleware import ACCESS_CONTROL_ALLOW_ORIGIN from django.contrib.auth.models import User from django.test import TestCase, override_settings from django.urls import reverse @@ -67,6 +68,7 @@ def test_get_content_public_version_anonymous_user(self, storage_mock): resp = self.get(self.url) self.assertEqual(resp.status_code, 200) self.assertIn("Content", resp.json()["content"]) + self.assertNotIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp.headers) def test_get_content_private_version_anonymous_user(self, storage_mock): self._mock_storage(storage_mock) @@ -85,6 +87,7 @@ def test_get_content_public_version_logged_in_user(self, storage_mock): resp = self.get(self.url) self.assertEqual(resp.status_code, 200) self.assertIn("Content", resp.json()["content"]) + self.assertNotIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp.headers) def test_get_content_private_version_logged_in_user(self, storage_mock): self._mock_storage(storage_mock) @@ -96,6 +99,7 @@ def test_get_content_private_version_logged_in_user(self, storage_mock): resp = self.get(self.url) self.assertEqual(resp.status_code, 200) self.assertIn("Content", resp.json()["content"]) + self.assertNotIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp.headers) @mock.patch.object(EmbedAPIBase, "_download_page_content") def test_get_content_allowed_external_page( @@ -108,6 +112,7 @@ def test_get_content_allowed_external_page( ) self.assertEqual(resp.status_code, 200) self.assertIn("Content", resp.json()["content"]) + self.assertNotIn(ACCESS_CONTROL_ALLOW_ORIGIN, resp.headers) def test_get_content_not_allowed_external_page(self, storage_mock): resp = self.get(reverse("embed_api_v3") + "?url=https://example.com/en/latest/") diff --git a/readthedocs/proxito/middleware.py b/readthedocs/proxito/middleware.py index dc9474fd5fd..e65bb1dd801 100644 --- a/readthedocs/proxito/middleware.py +++ b/readthedocs/proxito/middleware.py @@ -9,11 +9,14 @@ from urllib.parse import urlparse import structlog +from corsheaders.middleware import ( + ACCESS_CONTROL_ALLOW_METHODS, + ACCESS_CONTROL_ALLOW_ORIGIN, +) from django.conf import settings from django.core.exceptions import SuspiciousOperation from django.shortcuts import redirect from django.urls import reverse -from django.utils.cache import patch_vary_headers from django.utils.deprecation import MiddlewareMixin from readthedocs.builds.models import Version @@ -309,7 +312,7 @@ def add_hosting_integrations_headers(self, request, response): def add_cors_headers(self, request, response): """ - Add CORS headers only on PUBLIC versions. + Add CORS headers only to files from PUBLIC versions. DocDiff addons requires making a request from ``RTD_EXTERNAL_VERSION_DOMAIN`` to ``PUBLIC_DOMAIN`` to be able to @@ -318,6 +321,12 @@ def add_cors_headers(self, request, response): 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. + + We set this header to `*`, since the allowed versions are public only, + we don't care about the origin of the request. And we don't have the + need nor want to allow passing credentials from cross-origin requests. + + See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin. """ # Disable CORS on "Read the Docs for Business" for now. @@ -325,24 +334,22 @@ def add_cors_headers(self, request, response): if settings.ALLOW_PRIVATE_REPOS: return + # TODO: se should add these headers to files from docs only, + # proxied APIs and other endpoints should not have CORS headers. + # These attributes aren't currently set for proxied APIs, but we shuold + # find a better way to do this. project_slug = getattr(request, "path_project_slug", "") version_slug = getattr(request, "path_version_slug", "") - origin = request.headers.get("origin") - if ( - project_slug - and version_slug - and origin.endswith(settings.RTD_EXTERNAL_VERSION_DOMAIN) - ): + 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"] = origin - response.headers["Access-Control-Allow-Methods"] = "OPTIONS, GET" - patch_vary_headers(response, ("origin",)) + response.headers[ACCESS_CONTROL_ALLOW_ORIGIN] = "*" + response.headers[ACCESS_CONTROL_ALLOW_METHODS] = "HEAD, OPTIONS, GET" return response diff --git a/readthedocs/proxito/tests/test_headers.py b/readthedocs/proxito/tests/test_headers.py index 491af180ffd..df993a445f6 100644 --- a/readthedocs/proxito/tests/test_headers.py +++ b/readthedocs/proxito/tests/test_headers.py @@ -1,9 +1,16 @@ import django_dynamic_fixture as fixture +from corsheaders.middleware import ( + ACCESS_CONTROL_ALLOW_CREDENTIALS, + ACCESS_CONTROL_ALLOW_METHODS, + ACCESS_CONTROL_ALLOW_ORIGIN, +) from django.test import override_settings from django.urls import reverse +from django_dynamic_fixture import get from readthedocs.builds.constants import EXTERNAL, LATEST from readthedocs.builds.models import Version +from readthedocs.organizations.models import Organization from readthedocs.projects.constants import PRIVATE, PUBLIC from readthedocs.projects.models import Domain, HTTPHeader @@ -161,8 +168,9 @@ 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_non_external_domain(self): - fixture.get( + @override_settings(ALLOW_PRIVATE_REPOS=False) + def test_cors_headers_external_version(self): + get( Version, project=self.project, slug="111", @@ -171,74 +179,86 @@ def test_cors_headers_non_external_domain(self): type=EXTERNAL, ) + # Normal request r = self.client.get( - "/en/latest/", secure=True, headers={"host": "project.dev.readthedocs.io"} + "/en/111/", + secure=True, + headers={"host": "project--111.dev.readthedocs.build"}, ) 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_private_version(self): - fixture.get( - Version, - project=self.project, - slug="111", - active=True, - privacy_level=PRIVATE, - type=EXTERNAL, - ) + self.assertEqual(r[ACCESS_CONTROL_ALLOW_ORIGIN], "*") + self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, r.headers) + self.assertEqual(r[ACCESS_CONTROL_ALLOW_METHODS], "HEAD, OPTIONS, GET") + # Cross-origin request r = self.client.get( "/en/111/", secure=True, - headers={"origin": "project--111.dev.readthedocs.build"}, + headers={ + "host": "project--111.dev.readthedocs.build", + "origin": "https://example.com", + }, ) self.assertEqual(r.status_code, 200) - self.assertIsNone(r.get("Access-Control-Allow-Origin")) - self.assertIsNone(r.get("Access-Control-Allow-Methods")) - self.assertIsNone(r.get("Vary")) + self.assertEqual(r[ACCESS_CONTROL_ALLOW_ORIGIN], "*") + self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, r.headers) + self.assertEqual(r[ACCESS_CONTROL_ALLOW_METHODS], "HEAD, OPTIONS, GET") - def test_cors_headers_public_version(self): - fixture.get( - Version, - project=self.project, - slug="111", - active=True, - privacy_level=PUBLIC, - type=EXTERNAL, - ) + @override_settings(ALLOW_PRIVATE_REPOS=True, RTD_ALLOW_ORGANIZATIONS=True) + def test_cors_headers_private_version(self): + get(Organization, owners=[self.eric], projects=[self.project]) + self.version.privacy_level = PRIVATE + self.version.save() + + self.client.force_login(self.eric) + # Normal request r = self.client.get( - "/en/111/", + "/en/latest/", secure=True, - headers={"origin": "project--111.dev.readthedocs.build"}, + headers={"host": "project.dev.readthedocs.io"}, ) self.assertEqual(r.status_code, 200) - self.assertEqual( - r["Access-Control-Allow-Origin"], "project--111.dev.readthedocs.build" + self.assertNotIn(ACCESS_CONTROL_ALLOW_ORIGIN, r.headers) + + # Cross-origin request + r = self.client.get( + "/en/latest/", + secure=True, + headers={ + "host": "project.dev.readthedocs.io", + "origin": "https://example.com", + }, ) - self.assertEqual(r["Access-Control-Allow-Methods"], "OPTIONS, GET") - self.assertEqual(r["Vary"], "Origin") + self.assertEqual(r.status_code, 200) + self.assertNotIn(ACCESS_CONTROL_ALLOW_ORIGIN, r.headers) - @override_settings(ALLOW_PRIVATE_REPOS=True) - def test_cors_headers_public_version_allow_private_repositories(self): - fixture.get( - Version, - project=self.project, - slug="111", - active=True, - privacy_level=PUBLIC, - type=EXTERNAL, + @override_settings(ALLOW_PRIVATE_REPOS=False) + def test_cors_headers_public_version(self): + # Normal request + 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.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, r.headers) + self.assertEqual(r[ACCESS_CONTROL_ALLOW_METHODS], "HEAD, OPTIONS, GET") + # Cross-origin request r = self.client.get( - "/en/111/", + "/en/latest/", secure=True, - headers={"origin": "project--111.dev.readthedocs.build"}, + headers={ + "host": "project.dev.readthedocs.io", + "origin": "https://example.com", + }, ) self.assertEqual(r.status_code, 200) - self.assertIsNone(r.get("Access-Control-Allow-Origin")) - self.assertIsNone(r.get("Access-Control-Allow-Methods")) + self.assertEqual(r[ACCESS_CONTROL_ALLOW_ORIGIN], "*") + self.assertNotIn(ACCESS_CONTROL_ALLOW_CREDENTIALS, r.headers) + self.assertEqual(r[ACCESS_CONTROL_ALLOW_METHODS], "HEAD, OPTIONS, GET") @override_settings(ALLOW_PRIVATE_REPOS=False) def test_cache_headers_public_version_with_private_projects_not_allowed(self): diff --git a/readthedocs/search/tests/test_proxied_api.py b/readthedocs/search/tests/test_proxied_api.py index 5ebbb1e3931..8a22bb2b2b3 100644 --- a/readthedocs/search/tests/test_proxied_api.py +++ b/readthedocs/search/tests/test_proxied_api.py @@ -1,4 +1,5 @@ import pytest +from corsheaders.middleware import ACCESS_CONTROL_ALLOW_ORIGIN from readthedocs.search.tests.test_api import BaseTestDocumentSearch @@ -25,3 +26,4 @@ def test_headers(self, api_client, project): f"{project.slug},{project.slug}:{version.slug},{project.slug}:rtd-search" ) assert resp["Cache-Tag"] == cache_tags + assert ACCESS_CONTROL_ALLOW_ORIGIN not in resp.headers