mirror of https://github.com/scrapy/scrapy.git
235 lines
7.3 KiB
Python
235 lines
7.3 KiB
Python
"""Tests for scrapy.core.downloader.handlers.http11.HTTP11DownloadHandler."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from typing import TYPE_CHECKING, Any, cast
|
|
from unittest.mock import Mock
|
|
|
|
import pytest
|
|
from twisted.internet.testing import StringTransport
|
|
from twisted.web._newclient import Request as TxClientRequest
|
|
from twisted.web._newclient import RequestNotSent
|
|
from twisted.web.http_headers import Headers as TxHeaders
|
|
|
|
from scrapy import Spider
|
|
from scrapy.core.downloader.handlers.http11 import (
|
|
HTTP11DownloadHandler,
|
|
_ScrapyHTTP11ClientProtocol,
|
|
_TunnelingTCP4ClientEndpoint,
|
|
)
|
|
from scrapy.crawler import Crawler
|
|
from scrapy.exceptions import NotConfigured, ResponseHeadersTooLargeError
|
|
from scrapy.utils.misc import build_from_crawler
|
|
from tests.utils.bases.download_handlers_http import (
|
|
TestHttpBase,
|
|
TestHttpProxyBase,
|
|
TestHttpsBase,
|
|
TestHttpsCustomCiphersBase,
|
|
TestHttpsInvalidDNSIdBase,
|
|
TestHttpsInvalidDNSPatternBase,
|
|
TestHttpsTLSVersionBase,
|
|
TestHttpsWrongHostnameBase,
|
|
TestHttpWithCrawlerBase,
|
|
TestMitmProxyBase,
|
|
TestRealWebsiteBase,
|
|
TestSimpleHttpsBase,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from twisted.python.failure import Failure
|
|
|
|
from scrapy.core.downloader.handlers import DownloadHandlerProtocol
|
|
|
|
|
|
pytestmark = pytest.mark.requires_reactor # HTTP11DownloadHandler requires a reactor
|
|
|
|
|
|
class HTTP11DownloadHandlerMixin:
|
|
@property
|
|
def download_handler_cls(self) -> type[DownloadHandlerProtocol]:
|
|
return HTTP11DownloadHandler
|
|
|
|
@property
|
|
def settings_dict(self) -> dict[str, Any] | None:
|
|
return {
|
|
"DOWNLOAD_HANDLERS": {
|
|
"http": "scrapy.core.downloader.handlers.http11.HTTP11DownloadHandler",
|
|
"https": "scrapy.core.downloader.handlers.http11.HTTP11DownloadHandler",
|
|
}
|
|
}
|
|
|
|
|
|
def test_not_configured_without_reactor() -> None:
|
|
crawler = Crawler(Spider, {"TWISTED_REACTOR_ENABLED": False})
|
|
with pytest.raises(NotConfigured):
|
|
build_from_crawler(HTTP11DownloadHandler, crawler)
|
|
|
|
|
|
class TestHttp(HTTP11DownloadHandlerMixin, TestHttpBase):
|
|
pass
|
|
|
|
|
|
class TestHttps(HTTP11DownloadHandlerMixin, TestHttpsBase):
|
|
pass
|
|
|
|
|
|
class TestSimpleHttps(HTTP11DownloadHandlerMixin, TestSimpleHttpsBase):
|
|
pass
|
|
|
|
|
|
class TestHttpsWrongHostname(HTTP11DownloadHandlerMixin, TestHttpsWrongHostnameBase):
|
|
pass
|
|
|
|
|
|
class TestHttpsInvalidDNSId(HTTP11DownloadHandlerMixin, TestHttpsInvalidDNSIdBase):
|
|
pass
|
|
|
|
|
|
class TestHttpsInvalidDNSPattern(
|
|
HTTP11DownloadHandlerMixin, TestHttpsInvalidDNSPatternBase
|
|
):
|
|
pass
|
|
|
|
|
|
class TestHttpsCustomCiphers(HTTP11DownloadHandlerMixin, TestHttpsCustomCiphersBase):
|
|
pass
|
|
|
|
|
|
class TestHttpsTLSVersion(HTTP11DownloadHandlerMixin, TestHttpsTLSVersionBase):
|
|
pass
|
|
|
|
|
|
class TestHttpWithCrawler(HTTP11DownloadHandlerMixin, TestHttpWithCrawlerBase):
|
|
pass
|
|
|
|
|
|
class TestHttpsWithCrawler(TestHttpWithCrawler):
|
|
is_secure = True
|
|
|
|
|
|
class TestHttpProxy(HTTP11DownloadHandlerMixin, TestHttpProxyBase):
|
|
pass
|
|
|
|
|
|
class TestHttpsProxy(HTTP11DownloadHandlerMixin, TestHttpProxyBase):
|
|
is_secure = True
|
|
# not implemented
|
|
handler_supports_tls_in_tls = False
|
|
|
|
|
|
@pytest.mark.requires_mitmproxy
|
|
class TestMitmProxy(HTTP11DownloadHandlerMixin, TestMitmProxyBase):
|
|
# not implemented
|
|
handler_supports_tls_in_tls = False
|
|
|
|
|
|
@pytest.mark.requires_internet
|
|
class TestRealWebsite(HTTP11DownloadHandlerMixin, TestRealWebsiteBase):
|
|
@property
|
|
def platform_cert_store_works(self) -> bool:
|
|
return sys.platform != "win32"
|
|
|
|
|
|
class TestTunnelingHeadersMaxsize:
|
|
"""Tests for the DOWNLOAD_HEADERS_MAXSIZE limit that
|
|
``_TunnelingTCP4ClientEndpoint`` applies to the response head of the proxy,
|
|
which no HTTP client parses for us."""
|
|
|
|
def _get_endpoint(self, headers_maxsize: int) -> _TunnelingTCP4ClientEndpoint:
|
|
from twisted.internet import reactor
|
|
|
|
endpoint = _TunnelingTCP4ClientEndpoint(
|
|
reactor=cast("Any", reactor),
|
|
host="example.com",
|
|
port=443,
|
|
proxyConf=("proxy.example.com", 8080, None),
|
|
contextFactory=cast("Any", None),
|
|
headersMaxsize=headers_maxsize,
|
|
)
|
|
endpoint._protocol = cast("Any", Mock())
|
|
endpoint._protocolDataReceived = Mock()
|
|
return endpoint
|
|
|
|
def test_over_maxsize(self) -> None:
|
|
endpoint = self._get_endpoint(1024)
|
|
failures: list[Failure] = []
|
|
endpoint._tunnelReadyDeferred.addErrback(failures.append)
|
|
|
|
# A proxy response head that never ends.
|
|
for _ in range(3):
|
|
endpoint.processProxyResponse(b"a" * 512)
|
|
|
|
assert len(failures) == 1
|
|
assert failures[0].check(ResponseHeadersTooLargeError)
|
|
assert "1024" in str(failures[0].value)
|
|
assert "CONNECT example.com:443" in str(failures[0].value)
|
|
endpoint._protocol.transport.loseConnection.assert_called_once() # type: ignore[union-attr]
|
|
|
|
def test_under_maxsize(self) -> None:
|
|
endpoint = self._get_endpoint(1024)
|
|
failures: list[Failure] = []
|
|
endpoint._tunnelReadyDeferred.addErrback(failures.append)
|
|
|
|
endpoint.processProxyResponse(b"a" * 512)
|
|
|
|
assert not failures
|
|
|
|
def test_maxsize_disabled(self) -> None:
|
|
endpoint = self._get_endpoint(0)
|
|
failures: list[Failure] = []
|
|
endpoint._tunnelReadyDeferred.addErrback(failures.append)
|
|
|
|
for _ in range(3):
|
|
endpoint.processProxyResponse(b"a" * 512)
|
|
|
|
assert not failures
|
|
|
|
|
|
class TestScrapyHTTP11ClientProtocol:
|
|
"""Tests for the response header size limiting that
|
|
``_ScrapyHTTP11ClientProtocol`` installs on the response parser that
|
|
Twisted builds for each request."""
|
|
|
|
@staticmethod
|
|
def _get_protocol() -> _ScrapyHTTP11ClientProtocol:
|
|
protocol = _ScrapyHTTP11ClientProtocol(lambda _: None, 64 * 1024, 32 * 1024)
|
|
protocol.makeConnection(StringTransport()) # type: ignore[no-untyped-call]
|
|
return protocol
|
|
|
|
@staticmethod
|
|
def _get_request() -> TxClientRequest:
|
|
return TxClientRequest(
|
|
b"GET", b"/", TxHeaders({b"host": [b"example.com"]}), None
|
|
)
|
|
|
|
def test_parser_is_limited(self) -> None:
|
|
protocol = self._get_protocol()
|
|
protocol.request(self._get_request())
|
|
assert protocol._parser is not None
|
|
assert protocol._parser.MAX_LENGTH == 64 * 1024
|
|
# _limit_response_headers() overrides these on the instance.
|
|
assert "lineReceived" in vars(protocol._parser)
|
|
assert "lineLengthExceeded" in vars(protocol._parser)
|
|
|
|
def test_refused_request_keeps_previous_parser_limits(self) -> None:
|
|
"""A request that Twisted refuses leaves the parser of the previous
|
|
request untouched, so that its size counter is not reset while its
|
|
response is still being read."""
|
|
protocol = self._get_protocol()
|
|
protocol.request(self._get_request())
|
|
parser = protocol._parser
|
|
assert parser is not None
|
|
line_received = vars(parser)["lineReceived"]
|
|
|
|
# A second request over the same connection is refused, as the first one
|
|
# is still in progress.
|
|
deferred = protocol.request(self._get_request())
|
|
failures: list[Failure] = []
|
|
deferred.addErrback(failures.append)
|
|
|
|
assert len(failures) == 1
|
|
assert failures[0].check(RequestNotSent)
|
|
assert protocol._parser is parser
|
|
assert vars(parser)["lineReceived"] is line_received
|