scrapy/tests/test_spidermiddleware_refer...

1162 lines
38 KiB
Python

from __future__ import annotations
import warnings
from typing import TYPE_CHECKING, Any
from urllib.parse import urlparse
import pytest
from scrapy.exceptions import NotConfigured, ScrapyDeprecationWarning
from scrapy.http import Request, Response
from scrapy.settings import Settings
from scrapy.spidermiddlewares.referer import (
POLICY_NO_REFERRER,
POLICY_NO_REFERRER_WHEN_DOWNGRADE,
POLICY_ORIGIN,
POLICY_ORIGIN_WHEN_CROSS_ORIGIN,
POLICY_SAME_ORIGIN,
POLICY_SCRAPY_DEFAULT,
POLICY_STRICT_ORIGIN,
POLICY_STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
POLICY_UNSAFE_URL,
DefaultReferrerPolicy,
NoReferrerPolicy,
NoReferrerWhenDowngradePolicy,
OriginPolicy,
OriginWhenCrossOriginPolicy,
RefererMiddleware,
ReferrerPolicy,
SameOriginPolicy,
StrictOriginPolicy,
StrictOriginWhenCrossOriginPolicy,
UnsafeUrlPolicy,
)
from scrapy.utils.misc import build_from_crawler
from scrapy.utils.test import get_crawler
from tests.utils.decorators import coroutine_test
if TYPE_CHECKING:
from collections.abc import AsyncIterator
class TestRefererMiddleware:
req_meta: dict[str, Any] = {}
resp_headers: dict[str, str] = {}
settings: dict[str, Any] = {}
scenarii: list[tuple[str, str, bytes | None]] = [
("http://scrapytest.org", "http://scrapytest.org/", b"http://scrapytest.org"),
]
@pytest.fixture
def mw(self) -> RefererMiddleware:
settings = Settings(self.settings)
return RefererMiddleware(settings)
def get_request(self, target: str) -> Request:
return Request(target, meta=self.req_meta)
def get_response(self, origin: str) -> Response:
return Response(origin, headers=self.resp_headers)
def test(self, mw: RefererMiddleware) -> None:
for origin, target, referrer in self.scenarii:
response = self.get_response(origin)
request = self.get_request(target)
out = list(mw.process_spider_output(response, [request]))
assert out[0].headers.get("Referer") == referrer
class MixinDefault:
"""
Based on https://www.w3.org/TR/referrer-policy/#referrer-policy-no-referrer-when-downgrade
with some additional filtering of s3://
"""
scenarii: list[tuple[str, str, bytes | None]] = [
("https://example.com/", "https://scrapy.org/", b"https://example.com/"),
("http://example.com/", "http://scrapy.org/", b"http://example.com/"),
("http://example.com/", "https://scrapy.org/", b"http://example.com/"),
("https://example.com/", "http://scrapy.org/", None),
# no credentials leak
(
"http://user:password@example.com/",
"https://scrapy.org/",
b"http://example.com/",
),
# no referrer leak for local schemes
("file:///home/path/to/somefile.html", "https://scrapy.org/", None),
("file:///home/path/to/somefile.html", "http://scrapy.org/", None),
# no referrer leak for s3 origins
("s3://mybucket/path/to/data.csv", "https://scrapy.org/", None),
("s3://mybucket/path/to/data.csv", "http://scrapy.org/", None),
]
class MixinNoReferrer:
scenarii: list[tuple[str, str, bytes | None]] = [
("https://example.com/page.html", "https://example.com/", None),
("http://www.example.com/", "https://scrapy.org/", None),
("http://www.example.com/", "http://scrapy.org/", None),
("https://www.example.com/", "http://scrapy.org/", None),
("file:///home/path/to/somefile.html", "http://scrapy.org/", None),
]
class MixinNoReferrerWhenDowngrade:
scenarii: list[tuple[str, str, bytes | None]] = [
# TLS to TLS: send non-empty referrer
(
"https://example.com/page.html",
"https://not.example.com/",
b"https://example.com/page.html",
),
(
"https://example.com/page.html",
"https://scrapy.org/",
b"https://example.com/page.html",
),
(
"https://example.com:443/page.html",
"https://scrapy.org/",
b"https://example.com/page.html",
),
(
"https://example.com:444/page.html",
"https://scrapy.org/",
b"https://example.com:444/page.html",
),
(
"ftps://example.com/urls.zip",
"https://scrapy.org/",
b"ftps://example.com/urls.zip",
),
# TLS to non-TLS: do not send referrer
("https://example.com/page.html", "http://not.example.com/", None),
("https://example.com/page.html", "http://scrapy.org/", None),
("ftps://example.com/urls.zip", "http://scrapy.org/", None),
# non-TLS to TLS or non-TLS: send referrer
(
"http://example.com/page.html",
"https://not.example.com/",
b"http://example.com/page.html",
),
(
"http://example.com/page.html",
"https://scrapy.org/",
b"http://example.com/page.html",
),
(
"http://example.com:8080/page.html",
"https://scrapy.org/",
b"http://example.com:8080/page.html",
),
(
"http://example.com:80/page.html",
"http://not.example.com/",
b"http://example.com/page.html",
),
(
"http://example.com/page.html",
"http://scrapy.org/",
b"http://example.com/page.html",
),
(
"http://example.com:443/page.html",
"http://scrapy.org/",
b"http://example.com:443/page.html",
),
(
"ftp://example.com/urls.zip",
"http://scrapy.org/",
b"ftp://example.com/urls.zip",
),
(
"ftp://example.com/urls.zip",
"https://scrapy.org/",
b"ftp://example.com/urls.zip",
),
# test for user/password stripping
(
"http://user:password@example.com/page.html",
"https://not.example.com/",
b"http://example.com/page.html",
),
]
class MixinSameOrigin:
scenarii: list[tuple[str, str, bytes | None]] = [
# Same origin (protocol, host, port): send referrer
(
"https://example.com/page.html",
"https://example.com/not-page.html",
b"https://example.com/page.html",
),
(
"http://example.com/page.html",
"http://example.com/not-page.html",
b"http://example.com/page.html",
),
(
"https://example.com:443/page.html",
"https://example.com/not-page.html",
b"https://example.com/page.html",
),
(
"http://example.com:80/page.html",
"http://example.com/not-page.html",
b"http://example.com/page.html",
),
(
"http://example.com/page.html",
"http://example.com:80/not-page.html",
b"http://example.com/page.html",
),
(
"http://example.com:8888/page.html",
"http://example.com:8888/not-page.html",
b"http://example.com:8888/page.html",
),
# Different host: do NOT send referrer
(
"https://example.com/page.html",
"https://not.example.com/otherpage.html",
None,
),
("http://example.com/page.html", "http://not.example.com/otherpage.html", None),
("http://example.com/page.html", "http://www.example.com/otherpage.html", None),
# Different port: do NOT send referrer
(
"https://example.com:444/page.html",
"https://example.com/not-page.html",
None,
),
("http://example.com:81/page.html", "http://example.com/not-page.html", None),
("http://example.com/page.html", "http://example.com:81/not-page.html", None),
# Different protocols: do NOT send referrer
("https://example.com/page.html", "http://example.com/not-page.html", None),
("https://example.com/page.html", "http://not.example.com/", None),
("ftps://example.com/urls.zip", "https://example.com/not-page.html", None),
("ftp://example.com/urls.zip", "http://example.com/not-page.html", None),
("ftps://example.com/urls.zip", "https://example.com/not-page.html", None),
# test for user/password stripping
(
"https://user:password@example.com/page.html",
"http://example.com/not-page.html",
None,
),
(
"https://user:password@example.com/page.html",
"https://example.com/not-page.html",
b"https://example.com/page.html",
),
]
class MixinOrigin:
scenarii: list[tuple[str, str, bytes | None]] = [
# TLS or non-TLS to TLS or non-TLS: referrer origin is sent (yes, even for downgrades)
(
"https://example.com/page.html",
"https://example.com/not-page.html",
b"https://example.com/",
),
(
"https://example.com/page.html",
"https://scrapy.org",
b"https://example.com/",
),
("https://example.com/page.html", "http://scrapy.org", b"https://example.com/"),
("http://example.com/page.html", "http://scrapy.org", b"http://example.com/"),
# test for user/password stripping
(
"https://user:password@example.com/page.html",
"http://scrapy.org",
b"https://example.com/",
),
]
class MixinStrictOrigin:
scenarii: list[tuple[str, str, bytes | None]] = [
# TLS or non-TLS to TLS or non-TLS: referrer origin is sent but not for downgrades
(
"https://example.com/page.html",
"https://example.com/not-page.html",
b"https://example.com/",
),
(
"https://example.com/page.html",
"https://scrapy.org",
b"https://example.com/",
),
("http://example.com/page.html", "http://scrapy.org", b"http://example.com/"),
# downgrade: send nothing
("https://example.com/page.html", "http://scrapy.org", None),
# upgrade: send origin
("http://example.com/page.html", "https://scrapy.org", b"http://example.com/"),
# test for user/password stripping
(
"https://user:password@example.com/page.html",
"https://scrapy.org",
b"https://example.com/",
),
("https://user:password@example.com/page.html", "http://scrapy.org", None),
]
class MixinOriginWhenCrossOrigin:
scenarii: list[tuple[str, str, bytes | None]] = [
# Same origin (protocol, host, port): send referrer
(
"https://example.com/page.html",
"https://example.com/not-page.html",
b"https://example.com/page.html",
),
(
"http://example.com/page.html",
"http://example.com/not-page.html",
b"http://example.com/page.html",
),
(
"https://example.com:443/page.html",
"https://example.com/not-page.html",
b"https://example.com/page.html",
),
(
"http://example.com:80/page.html",
"http://example.com/not-page.html",
b"http://example.com/page.html",
),
(
"http://example.com/page.html",
"http://example.com:80/not-page.html",
b"http://example.com/page.html",
),
(
"http://example.com:8888/page.html",
"http://example.com:8888/not-page.html",
b"http://example.com:8888/page.html",
),
# Different host: send origin as referrer
(
"https://example2.com/page.html",
"https://scrapy.org/otherpage.html",
b"https://example2.com/",
),
(
"https://example2.com/page.html",
"https://not.example2.com/otherpage.html",
b"https://example2.com/",
),
(
"http://example2.com/page.html",
"http://not.example2.com/otherpage.html",
b"http://example2.com/",
),
# exact match required
(
"http://example2.com/page.html",
"http://www.example2.com/otherpage.html",
b"http://example2.com/",
),
# Different port: send origin as referrer
(
"https://example3.com:444/page.html",
"https://example3.com/not-page.html",
b"https://example3.com:444/",
),
(
"http://example3.com:81/page.html",
"http://example3.com/not-page.html",
b"http://example3.com:81/",
),
# Different protocols: send origin as referrer
(
"https://example4.com/page.html",
"http://example4.com/not-page.html",
b"https://example4.com/",
),
(
"https://example4.com/page.html",
"http://not.example4.com/",
b"https://example4.com/",
),
(
"ftps://example4.com/urls.zip",
"https://example4.com/not-page.html",
b"ftps://example4.com/",
),
(
"ftp://example4.com/urls.zip",
"http://example4.com/not-page.html",
b"ftp://example4.com/",
),
(
"ftps://example4.com/urls.zip",
"https://example4.com/not-page.html",
b"ftps://example4.com/",
),
# test for user/password stripping
(
"https://user:password@example5.com/page.html",
"https://example5.com/not-page.html",
b"https://example5.com/page.html",
),
# TLS to non-TLS downgrade: send origin
(
"https://user:password@example5.com/page.html",
"http://example5.com/not-page.html",
b"https://example5.com/",
),
]
class MixinStrictOriginWhenCrossOrigin:
scenarii: list[tuple[str, str, bytes | None]] = [
# Same origin (protocol, host, port): send referrer
(
"https://example.com/page.html",
"https://example.com/not-page.html",
b"https://example.com/page.html",
),
(
"http://example.com/page.html",
"http://example.com/not-page.html",
b"http://example.com/page.html",
),
(
"https://example.com:443/page.html",
"https://example.com/not-page.html",
b"https://example.com/page.html",
),
(
"http://example.com:80/page.html",
"http://example.com/not-page.html",
b"http://example.com/page.html",
),
(
"http://example.com/page.html",
"http://example.com:80/not-page.html",
b"http://example.com/page.html",
),
(
"http://example.com:8888/page.html",
"http://example.com:8888/not-page.html",
b"http://example.com:8888/page.html",
),
# Different host: send origin as referrer
(
"https://example2.com/page.html",
"https://scrapy.org/otherpage.html",
b"https://example2.com/",
),
(
"https://example2.com/page.html",
"https://not.example2.com/otherpage.html",
b"https://example2.com/",
),
(
"http://example2.com/page.html",
"http://not.example2.com/otherpage.html",
b"http://example2.com/",
),
# exact match required
(
"http://example2.com/page.html",
"http://www.example2.com/otherpage.html",
b"http://example2.com/",
),
# Different port: send origin as referrer
(
"https://example3.com:444/page.html",
"https://example3.com/not-page.html",
b"https://example3.com:444/",
),
(
"http://example3.com:81/page.html",
"http://example3.com/not-page.html",
b"http://example3.com:81/",
),
# downgrade
("https://example4.com/page.html", "http://example4.com/not-page.html", None),
("https://example4.com/page.html", "http://not.example4.com/", None),
# non-TLS to non-TLS
(
"ftp://example4.com/urls.zip",
"http://example4.com/not-page.html",
b"ftp://example4.com/",
),
# upgrade
(
"http://example4.com/page.html",
"https://example4.com/not-page.html",
b"http://example4.com/",
),
(
"http://example4.com/page.html",
"https://not.example4.com/",
b"http://example4.com/",
),
# Different protocols: send origin as referrer
(
"ftps://example4.com/urls.zip",
"https://example4.com/not-page.html",
b"ftps://example4.com/",
),
(
"ftps://example4.com/urls.zip",
"https://example4.com/not-page.html",
b"ftps://example4.com/",
),
# test for user/password stripping
(
"https://user:password@example5.com/page.html",
"https://example5.com/not-page.html",
b"https://example5.com/page.html",
),
# TLS to non-TLS downgrade: send nothing
(
"https://user:password@example5.com/page.html",
"http://example5.com/not-page.html",
None,
),
]
class MixinUnsafeUrl:
scenarii: list[tuple[str, str, bytes | None]] = [
# TLS to TLS: send referrer
(
"https://example.com/sekrit.html",
"http://not.example.com/",
b"https://example.com/sekrit.html",
),
(
"https://example1.com/page.html",
"https://not.example1.com/",
b"https://example1.com/page.html",
),
(
"https://example1.com/page.html",
"https://scrapy.org/",
b"https://example1.com/page.html",
),
(
"https://example1.com:443/page.html",
"https://scrapy.org/",
b"https://example1.com/page.html",
),
(
"https://example1.com:444/page.html",
"https://scrapy.org/",
b"https://example1.com:444/page.html",
),
(
"ftps://example1.com/urls.zip",
"https://scrapy.org/",
b"ftps://example1.com/urls.zip",
),
# TLS to non-TLS: send referrer (yes, it's unsafe)
(
"https://example2.com/page.html",
"http://not.example2.com/",
b"https://example2.com/page.html",
),
(
"https://example2.com/page.html",
"http://scrapy.org/",
b"https://example2.com/page.html",
),
(
"ftps://example2.com/urls.zip",
"http://scrapy.org/",
b"ftps://example2.com/urls.zip",
),
# non-TLS to TLS or non-TLS: send referrer (yes, it's unsafe)
(
"http://example3.com/page.html",
"https://not.example3.com/",
b"http://example3.com/page.html",
),
(
"http://example3.com/page.html",
"https://scrapy.org/",
b"http://example3.com/page.html",
),
(
"http://example3.com:8080/page.html",
"https://scrapy.org/",
b"http://example3.com:8080/page.html",
),
(
"http://example3.com:80/page.html",
"http://not.example3.com/",
b"http://example3.com/page.html",
),
(
"http://example3.com/page.html",
"http://scrapy.org/",
b"http://example3.com/page.html",
),
(
"http://example3.com:443/page.html",
"http://scrapy.org/",
b"http://example3.com:443/page.html",
),
(
"ftp://example3.com/urls.zip",
"http://scrapy.org/",
b"ftp://example3.com/urls.zip",
),
(
"ftp://example3.com/urls.zip",
"https://scrapy.org/",
b"ftp://example3.com/urls.zip",
),
# test for user/password stripping
(
"http://user:password@example4.com/page.html",
"https://not.example4.com/",
b"http://example4.com/page.html",
),
(
"https://user:password@example4.com/page.html",
"http://scrapy.org/",
b"https://example4.com/page.html",
),
]
class TestRefererMiddlewareDefault(MixinDefault, TestRefererMiddleware):
pass
# --- Tests using settings to set policy using class path
class TestSettingsNoReferrer(MixinNoReferrer, TestRefererMiddleware):
settings = {"REFERRER_POLICY": "scrapy.spidermiddlewares.referer.NoReferrerPolicy"}
class TestSettingsNoReferrerWhenDowngrade(
MixinNoReferrerWhenDowngrade, TestRefererMiddleware
):
settings = {
"REFERRER_POLICY": "scrapy.spidermiddlewares.referer.NoReferrerWhenDowngradePolicy"
}
class TestSettingsSameOrigin(MixinSameOrigin, TestRefererMiddleware):
settings = {"REFERRER_POLICY": "scrapy.spidermiddlewares.referer.SameOriginPolicy"}
class TestSettingsOrigin(MixinOrigin, TestRefererMiddleware):
settings = {"REFERRER_POLICY": "scrapy.spidermiddlewares.referer.OriginPolicy"}
class TestSettingsStrictOrigin(MixinStrictOrigin, TestRefererMiddleware):
settings = {
"REFERRER_POLICY": "scrapy.spidermiddlewares.referer.StrictOriginPolicy"
}
class TestSettingsOriginWhenCrossOrigin(
MixinOriginWhenCrossOrigin, TestRefererMiddleware
):
settings = {
"REFERRER_POLICY": "scrapy.spidermiddlewares.referer.OriginWhenCrossOriginPolicy"
}
class TestSettingsStrictOriginWhenCrossOrigin(
MixinStrictOriginWhenCrossOrigin, TestRefererMiddleware
):
settings = {
"REFERRER_POLICY": "scrapy.spidermiddlewares.referer.StrictOriginWhenCrossOriginPolicy"
}
class TestSettingsUnsafeUrl(MixinUnsafeUrl, TestRefererMiddleware):
settings = {"REFERRER_POLICY": "scrapy.spidermiddlewares.referer.UnsafeUrlPolicy"}
class CustomPythonOrgPolicy(ReferrerPolicy):
"""
A dummy policy that returns referrer as http(s)://python.org
depending on the scheme of the target URL.
"""
def referrer(self, response, request):
scheme = urlparse(request).scheme
if scheme == "https":
return b"https://python.org/"
if scheme == "http":
return b"http://python.org/"
return None
class TestSettingsCustomPolicy(TestRefererMiddleware):
settings = {"REFERRER_POLICY": CustomPythonOrgPolicy}
scenarii = [
("https://example.com/", "https://scrapy.org/", b"https://python.org/"),
("http://example.com/", "http://scrapy.org/", b"http://python.org/"),
("http://example.com/", "https://scrapy.org/", b"https://python.org/"),
("https://example.com/", "http://scrapy.org/", b"http://python.org/"),
(
"file:///home/path/to/somefile.html",
"https://scrapy.org/",
b"https://python.org/",
),
(
"file:///home/path/to/somefile.html",
"http://scrapy.org/",
b"http://python.org/",
),
]
# --- Tests using Request meta dict to set policy
class TestRequestMetaDefault(MixinDefault, TestRefererMiddleware):
req_meta = {"referrer_policy": POLICY_SCRAPY_DEFAULT}
class TestRequestMetaNoReferrer(MixinNoReferrer, TestRefererMiddleware):
req_meta = {"referrer_policy": POLICY_NO_REFERRER}
class TestRequestMetaNoReferrerWhenDowngrade(
MixinNoReferrerWhenDowngrade, TestRefererMiddleware
):
req_meta = {"referrer_policy": POLICY_NO_REFERRER_WHEN_DOWNGRADE}
class TestRequestMetaSameOrigin(MixinSameOrigin, TestRefererMiddleware):
req_meta = {"referrer_policy": POLICY_SAME_ORIGIN}
class TestRequestMetaOrigin(MixinOrigin, TestRefererMiddleware):
req_meta = {"referrer_policy": POLICY_ORIGIN}
class TestRequestMetaSrictOrigin(MixinStrictOrigin, TestRefererMiddleware):
req_meta = {"referrer_policy": POLICY_STRICT_ORIGIN}
class TestRequestMetaOriginWhenCrossOrigin(
MixinOriginWhenCrossOrigin, TestRefererMiddleware
):
req_meta = {"referrer_policy": POLICY_ORIGIN_WHEN_CROSS_ORIGIN}
class TestRequestMetaStrictOriginWhenCrossOrigin(
MixinStrictOriginWhenCrossOrigin, TestRefererMiddleware
):
req_meta = {"referrer_policy": POLICY_STRICT_ORIGIN_WHEN_CROSS_ORIGIN}
class TestRequestMetaUnsafeUrl(MixinUnsafeUrl, TestRefererMiddleware):
req_meta = {"referrer_policy": POLICY_UNSAFE_URL}
class TestRequestMetaPrecedence001(MixinUnsafeUrl, TestRefererMiddleware):
settings = {"REFERRER_POLICY": "scrapy.spidermiddlewares.referer.SameOriginPolicy"}
req_meta = {"referrer_policy": POLICY_UNSAFE_URL}
class TestRequestMetaPrecedence002(MixinNoReferrer, TestRefererMiddleware):
settings = {
"REFERRER_POLICY": "scrapy.spidermiddlewares.referer.NoReferrerWhenDowngradePolicy"
}
req_meta = {"referrer_policy": POLICY_NO_REFERRER}
class TestRequestMetaPrecedence003(MixinUnsafeUrl, TestRefererMiddleware):
settings = {
"REFERRER_POLICY": "scrapy.spidermiddlewares.referer.OriginWhenCrossOriginPolicy"
}
req_meta = {"referrer_policy": POLICY_UNSAFE_URL}
class TestRequestMetaSettingFallback:
params = [
(
# When an unknown policy is referenced in Request.meta
# (here, a typo error),
# the policy defined in settings takes precedence
{
"REFERRER_POLICY": "scrapy.spidermiddlewares.referer.OriginWhenCrossOriginPolicy"
},
{},
{"referrer_policy": "ssscrapy-default"},
OriginWhenCrossOriginPolicy,
True,
),
(
# same as above but with string value for settings policy
{"REFERRER_POLICY": "origin-when-cross-origin"},
{},
{"referrer_policy": "ssscrapy-default"},
OriginWhenCrossOriginPolicy,
True,
),
(
# request meta references a wrong policy but it is set,
# so the Referrer-Policy header in response is not used,
# and the settings' policy is applied
{"REFERRER_POLICY": "origin-when-cross-origin"},
{"Referrer-Policy": "unsafe-url"},
{"referrer_policy": "ssscrapy-default"},
OriginWhenCrossOriginPolicy,
True,
),
(
# here, request meta does not set the policy
# so response headers take precedence
{"REFERRER_POLICY": "origin-when-cross-origin"},
{"Referrer-Policy": "unsafe-url"},
{},
UnsafeUrlPolicy,
False,
),
(
# here, request meta does not set the policy,
# but response headers also use an unknown policy,
# so the settings' policy is used
{"REFERRER_POLICY": "origin-when-cross-origin"},
{"Referrer-Policy": "unknown"},
{},
OriginWhenCrossOriginPolicy,
True,
),
]
def test(self):
origin = "http://www.scrapy.org"
target = "http://www.example.com"
for (
settings,
response_headers,
request_meta,
policy_class,
check_warning,
) in self.params:
mw = RefererMiddleware(Settings(settings))
response = Response(origin, headers=response_headers)
request = Request(target, meta=request_meta)
if check_warning:
with pytest.warns(
RuntimeWarning, match="Could not load referrer policy"
):
policy = mw.policy(response, request)
else:
policy = mw.policy(response, request)
assert isinstance(policy, policy_class)
class TestSettingsPolicyByName:
def test_valid_name(self):
for s, p in [
(POLICY_SCRAPY_DEFAULT, DefaultReferrerPolicy),
(POLICY_NO_REFERRER, NoReferrerPolicy),
(POLICY_NO_REFERRER_WHEN_DOWNGRADE, NoReferrerWhenDowngradePolicy),
(POLICY_SAME_ORIGIN, SameOriginPolicy),
(POLICY_ORIGIN, OriginPolicy),
(POLICY_STRICT_ORIGIN, StrictOriginPolicy),
(POLICY_ORIGIN_WHEN_CROSS_ORIGIN, OriginWhenCrossOriginPolicy),
(POLICY_STRICT_ORIGIN_WHEN_CROSS_ORIGIN, StrictOriginWhenCrossOriginPolicy),
(POLICY_UNSAFE_URL, UnsafeUrlPolicy),
]:
settings = Settings({"REFERRER_POLICY": s})
mw = RefererMiddleware(settings)
assert mw.default_policy == p
def test_valid_name_casevariants(self):
for s, p in [
(POLICY_SCRAPY_DEFAULT, DefaultReferrerPolicy),
(POLICY_NO_REFERRER, NoReferrerPolicy),
(POLICY_NO_REFERRER_WHEN_DOWNGRADE, NoReferrerWhenDowngradePolicy),
(POLICY_SAME_ORIGIN, SameOriginPolicy),
(POLICY_ORIGIN, OriginPolicy),
(POLICY_STRICT_ORIGIN, StrictOriginPolicy),
(POLICY_ORIGIN_WHEN_CROSS_ORIGIN, OriginWhenCrossOriginPolicy),
(POLICY_STRICT_ORIGIN_WHEN_CROSS_ORIGIN, StrictOriginWhenCrossOriginPolicy),
(POLICY_UNSAFE_URL, UnsafeUrlPolicy),
]:
settings = Settings({"REFERRER_POLICY": s.upper()})
mw = RefererMiddleware(settings)
assert mw.default_policy == p
def test_invalid_name(self):
settings = Settings({"REFERRER_POLICY": "some-custom-unknown-policy"})
with pytest.raises(RuntimeError):
RefererMiddleware(settings)
def test_multiple_policy_tokens(self):
# test parsing without space(s) after the comma
settings1 = Settings(
{
"REFERRER_POLICY": (
f"some-custom-unknown-policy,"
f"{POLICY_SAME_ORIGIN},"
f"{POLICY_STRICT_ORIGIN_WHEN_CROSS_ORIGIN},"
f"another-custom-unknown-policy"
)
}
)
mw1 = RefererMiddleware(settings1)
assert mw1.default_policy == StrictOriginWhenCrossOriginPolicy
# test parsing with space(s) after the comma
settings2 = Settings(
{
"REFERRER_POLICY": (
f"{POLICY_STRICT_ORIGIN_WHEN_CROSS_ORIGIN},"
f" another-custom-unknown-policy,"
f" {POLICY_UNSAFE_URL}"
)
}
)
mw2 = RefererMiddleware(settings2)
assert mw2.default_policy == UnsafeUrlPolicy
def test_multiple_policy_tokens_all_invalid(self):
settings = Settings(
{
"REFERRER_POLICY": (
"some-custom-unknown-policy,"
"another-custom-unknown-policy,"
"yet-another-custom-unknown-policy"
)
}
)
with pytest.raises(RuntimeError):
RefererMiddleware(settings)
class TestPolicyHeaderPrecedence001(MixinUnsafeUrl, TestRefererMiddleware):
settings = {"REFERRER_POLICY": "scrapy.spidermiddlewares.referer.SameOriginPolicy"}
resp_headers = {"Referrer-Policy": POLICY_UNSAFE_URL.upper()}
class TestPolicyHeaderPrecedence002(MixinNoReferrer, TestRefererMiddleware):
settings = {
"REFERRER_POLICY": "scrapy.spidermiddlewares.referer.NoReferrerWhenDowngradePolicy"
}
resp_headers = {"Referrer-Policy": POLICY_NO_REFERRER.swapcase()}
class TestPolicyHeaderPrecedence003(
MixinNoReferrerWhenDowngrade, TestRefererMiddleware
):
settings = {
"REFERRER_POLICY": "scrapy.spidermiddlewares.referer.OriginWhenCrossOriginPolicy"
}
resp_headers = {"Referrer-Policy": POLICY_NO_REFERRER_WHEN_DOWNGRADE.title()}
class TestPolicyHeaderPrecedence004(
MixinNoReferrerWhenDowngrade, TestRefererMiddleware
):
"""
The empty string means "no-referrer-when-downgrade"
"""
settings = {
"REFERRER_POLICY": "scrapy.spidermiddlewares.referer.OriginWhenCrossOriginPolicy"
}
resp_headers = {"Referrer-Policy": ""}
class TestPolicyMethodResponseParamRename:
def setup_method(self):
self.crawler = get_crawler()
self.mw = build_from_crawler(RefererMiddleware, self.crawler)
self.request = Request("http://www.example.com")
self.response = Response("http://www.example.com")
def test_pos_string(self):
with pytest.warns(
ScrapyDeprecationWarning,
match=r"Passing a response URL to RefererMiddleware\.policy\(\)",
):
self.mw.policy("http://old.com", self.request)
def test_pos_response(self):
with warnings.catch_warnings():
warnings.filterwarnings(
"error",
category=ScrapyDeprecationWarning,
message=r"Passing 'resp_or_url' is deprecated",
)
self.mw.policy(self.response, self.request)
def test_key_resp_or_url(self):
with pytest.warns(
ScrapyDeprecationWarning, match=r"Passing 'resp_or_url' is deprecated"
):
self.mw.policy(resp_or_url=self.response, request=self.request)
def test_key_response(self):
with warnings.catch_warnings():
warnings.filterwarnings(
"error",
category=ScrapyDeprecationWarning,
message=r"Passing 'resp_or_url' is deprecated",
)
self.mw.policy(response=self.response, request=self.request)
def test_key_response_string(self):
with pytest.warns(ScrapyDeprecationWarning, match="Passing a response URL"):
self.mw.policy(response="http://old.com", request=self.request)
def test_both_resp_or_url_and_response(self):
with pytest.raises(
TypeError, match="Cannot pass both 'response' and 'resp_or_url'"
):
self.mw.policy(
response=self.response, resp_or_url=self.response, request=self.request
)
def test_missing_response(self):
with pytest.raises(TypeError, match="Missing required argument: 'response'"):
self.mw.policy(request=self.request)
def test_missing_request(self):
with pytest.raises(TypeError, match="Missing required argument: 'request'"):
self.mw.policy(response=self.response)
@coroutine_test
async def test_response_policy_only_supports_policy_names():
crawler = get_crawler(settings_dict={"REFERRER_POLICY": "no-referrer"})
mw = build_from_crawler(RefererMiddleware, crawler)
async def input_result() -> AsyncIterator[Any]:
yield Request("https://example.com/")
response = Response(
"https://example.com/",
headers={
"Referrer-Policy": "scrapy.spidermiddlewares.referer.NoReferrerWhenDowngradePolicy"
},
)
with pytest.warns(
RuntimeWarning,
match=r"Could not load referrer policy 'scrapy\.spidermiddlewares\.referer\.NoReferrerWhenDowngradePolicy' \(import paths from the response Referrer-Policy header are not allowed\)",
):
output = [
request
async for request in mw.process_spider_output_async(
response, input_result()
)
]
assert len(output) == 1
assert b"Referer" not in output[0].headers
response = Response(
"https://example.com/",
headers={"Referrer-Policy": "no-referrer-when-downgrade"},
)
output = [
request
async for request in mw.process_spider_output_async(response, input_result())
]
assert len(output) == 1
assert output[0].headers == {b"Referer": [b"https://example.com/"]}
@coroutine_test
async def test_referer_policies_setting():
crawler = get_crawler(
settings_dict={
"REFERRER_POLICY": "no-referrer",
"REFERRER_POLICIES": {
"no-referrer-when-downgrade": None,
"custom-policy": CustomPythonOrgPolicy,
"": CustomPythonOrgPolicy,
},
}
)
mw = build_from_crawler(RefererMiddleware, crawler)
async def input_result() -> AsyncIterator[Any]:
yield Request("https://example.com/")
# "no-referrer-when-downgrade": None,
response = Response(
"https://example.com/",
headers={"Referrer-Policy": "no-referrer-when-downgrade"},
)
with pytest.warns(
RuntimeWarning,
match=r"Could not load referrer policy 'no-referrer-when-downgrade'",
):
output = [
request
async for request in mw.process_spider_output_async(
response, input_result()
)
]
assert len(output) == 1
assert b"Referer" not in output[0].headers
# "custom-policy": CustomPythonOrgPolicy,
response = Response(
"https://example.com/",
headers={"Referrer-Policy": "custom-policy"},
)
output = [
request
async for request in mw.process_spider_output_async(response, input_result())
]
assert len(output) == 1
assert output[0].headers == {b"Referer": [b"https://python.org/"]}
# "": CustomPythonOrgPolicy,
response = Response(
"https://example.com/",
headers={"Referrer-Policy": ""},
)
output = [
request
async for request in mw.process_spider_output_async(response, input_result())
]
assert len(output) == 1
assert output[0].headers == {b"Referer": [b"https://python.org/"]}
class TestReferrerPolicyHelpers:
def test_origin_referrer_local_scheme(self):
# A local scheme yields no referrer.
assert UnsafeUrlPolicy().origin_referrer("data:,foo") is None
def test_strip_url_empty(self):
assert UnsafeUrlPolicy().strip_url("") is None
def test_potentially_trustworthy_data_scheme(self):
assert UnsafeUrlPolicy().potentially_trustworthy("data:,foo") is False
def test_default_policy():
crawler = get_crawler()
mw = build_from_crawler(RefererMiddleware, crawler)
assert mw.default_policy is DefaultReferrerPolicy
def test_no_settings_constructor():
with pytest.warns(
ScrapyDeprecationWarning,
match="Instantiating RefererMiddleware without a 'settings' argument",
):
mw = RefererMiddleware()
assert mw.default_policy is DefaultReferrerPolicy
def test_not_configured_when_disabled():
crawler = get_crawler(settings_dict={"REFERER_ENABLED": False})
with pytest.raises(NotConfigured):
build_from_crawler(RefererMiddleware, crawler)