Mind Spider.download_maxsize and Request.meta['download_maxsize']

This commit is contained in:
Adrián Chaves 2023-11-22 19:03:24 +01:00
parent 3fda2fe103
commit e0b66c021a
4 changed files with 143 additions and 31 deletions

View File

@ -1,5 +1,6 @@
import warnings import warnings
from scrapy import signals
from scrapy.exceptions import IgnoreRequest, NotConfigured from scrapy.exceptions import IgnoreRequest, NotConfigured
from scrapy.http import Response, TextResponse from scrapy.http import Response, TextResponse
from scrapy.responsetypes import responsetypes from scrapy.responsetypes import responsetypes
@ -38,6 +39,7 @@ class HttpCompressionMiddleware:
return return
self.stats = crawler.stats self.stats = crawler.stats
self._max_size = crawler.settings.getint("DOWNLOAD_MAXSIZE") self._max_size = crawler.settings.getint("DOWNLOAD_MAXSIZE")
crawler.signals.connect(self.open_spider, signals.spider_opened)
@classmethod @classmethod
def from_crawler(cls, crawler): def from_crawler(cls, crawler):
@ -52,10 +54,15 @@ class HttpCompressionMiddleware:
"reimplement their 'from_crawler' method.", "reimplement their 'from_crawler' method.",
ScrapyDeprecationWarning, ScrapyDeprecationWarning,
) )
result = cls() spider = cls()
result.stats = crawler.stats spider.stats = crawler.stats
result._max_size = crawler.settings.getint("DOWNLOAD_MAXSIZE") spider._max_size = crawler.settings.getint("DOWNLOAD_MAXSIZE")
return result crawler.signals.connect(spider.open_spider, signals.spider_opened)
return spider
def open_spider(self, spider):
if hasattr(spider, "download_maxsize"):
self._max_size = spider.download_maxsize
def process_request(self, request, spider): def process_request(self, request, spider):
request.headers.setdefault("Accept-Encoding", b", ".join(ACCEPTED_ENCODINGS)) request.headers.setdefault("Accept-Encoding", b", ".join(ACCEPTED_ENCODINGS))
@ -67,8 +74,11 @@ class HttpCompressionMiddleware:
content_encoding = response.headers.getlist("Content-Encoding") content_encoding = response.headers.getlist("Content-Encoding")
if content_encoding: if content_encoding:
encoding = content_encoding.pop() encoding = content_encoding.pop()
max_size = request.meta.get("download_maxsize", self._max_size)
try: try:
decoded_body = self._decode(response.body, encoding.lower()) decoded_body = self._decode(
response.body, encoding.lower(), max_size
)
except _DecompressionMaxSizeExceeded: except _DecompressionMaxSizeExceeded:
raise IgnoreRequest( raise IgnoreRequest(
f"Ignored response {response} because its body " f"Ignored response {response} because its body "
@ -98,13 +108,13 @@ class HttpCompressionMiddleware:
return response return response
def _decode(self, body, encoding): def _decode(self, body, encoding, max_size):
if encoding == b"gzip" or encoding == b"x-gzip": if encoding == b"gzip" or encoding == b"x-gzip":
return gunzip(body, max_size=self._max_size) return gunzip(body, max_size=max_size)
if encoding == b"deflate": if encoding == b"deflate":
return _inflate(body, max_size=self._max_size) return _inflate(body, max_size=max_size)
if encoding == b"br" and b"br" in ACCEPTED_ENCODINGS: if encoding == b"br" and b"br" in ACCEPTED_ENCODINGS:
return _unbrotli(body, max_size=self._max_size) return _unbrotli(body, max_size=max_size)
if encoding == b"zstd" and b"zstd" in ACCEPTED_ENCODINGS: if encoding == b"zstd" and b"zstd" in ACCEPTED_ENCODINGS:
return _unzstd(body, max_size=self._max_size) return _unzstd(body, max_size=max_size)
return body return body

View File

@ -72,10 +72,14 @@ class SitemapSpider(Spider):
if isinstance(response, XmlResponse): if isinstance(response, XmlResponse):
return response.body return response.body
if gzip_magic_number(response): if gzip_magic_number(response):
max_size = response.meta.get(
"download_maxsize",
getattr(
self, "download_maxsize", self.settings.getint("DOWNLOAD_MAXSIZE")
),
)
try: try:
return gunzip( return gunzip(response.body, max_size=max_size)
response.body, max_size=self.settings.getint("DOWNLOAD_MAXSIZE")
)
except _DecompressionMaxSizeExceeded: except _DecompressionMaxSizeExceeded:
return None return None
# actual gzipped sitemap files are decompressed above ; # actual gzipped sitemap files are decompressed above ;

View File

@ -49,10 +49,7 @@ FORMAT = {
class HttpCompressionTest(TestCase): class HttpCompressionTest(TestCase):
def setUp(self): def setUp(self):
settings = { self.crawler = get_crawler(Spider)
"DOWNLOAD_MAXSIZE": 10_000_000, # For compression bomb tests.
}
self.crawler = get_crawler(Spider, settings_dict=settings)
self.spider = self.crawler._create_spider("scrapytest.org") self.spider = self.crawler._create_spider("scrapytest.org")
self.mw = HttpCompressionMiddleware.from_crawler(self.crawler) self.mw = HttpCompressionMiddleware.from_crawler(self.crawler)
self.crawler.stats.open_spider(self.spider) self.crawler.stats.open_spider(self.spider)
@ -373,31 +370,103 @@ class HttpCompressionTest(TestCase):
self.assertStatsEqual("httpcompression/response_count", None) self.assertStatsEqual("httpcompression/response_count", None)
self.assertStatsEqual("httpcompression/response_bytes", None) self.assertStatsEqual("httpcompression/response_bytes", None)
def _test_compression_bomb(self, compression_id): def _test_compression_bomb_setting(self, compression_id):
settings = {"DOWNLOAD_MAXSIZE": 10_000_000}
crawler = get_crawler(Spider, settings_dict=settings)
spider = crawler._create_spider("scrapytest.org")
mw = HttpCompressionMiddleware.from_crawler(crawler)
mw.open_spider(spider)
response = self._getresponse(f"bomb-{compression_id}") response = self._getresponse(f"bomb-{compression_id}")
self.assertRaises( self.assertRaises(
IgnoreRequest, IgnoreRequest,
self.mw.process_response, mw.process_response,
response.request, response.request,
response, response,
self.spider, spider,
) )
def test_compression_bomb_br(self): def test_compression_bomb_setting_br(self):
try: try:
import brotli # noqa: F401 import brotli # noqa: F401
except ImportError: except ImportError:
raise SkipTest("no brotli") raise SkipTest("no brotli")
self._test_compression_bomb("br") self._test_compression_bomb_setting("br")
def test_compression_bomb_deflate(self): def test_compression_bomb_setting_deflate(self):
self._test_compression_bomb("deflate") self._test_compression_bomb_setting("deflate")
def test_compression_bomb_gzip(self): def test_compression_bomb_setting_gzip(self):
self._test_compression_bomb("gzip") self._test_compression_bomb_setting("gzip")
def test_compression_bomb_zstd(self): def test_compression_bomb_setting_zstd(self):
self._test_compression_bomb("zstd") self._test_compression_bomb_setting("zstd")
def _test_compression_bomb_spider_attr(self, compression_id):
class DownloadMaxSizeSpider(Spider):
download_maxsize = 10_000_000
crawler = get_crawler(DownloadMaxSizeSpider)
spider = crawler._create_spider("scrapytest.org")
mw = HttpCompressionMiddleware.from_crawler(crawler)
mw.open_spider(spider)
response = self._getresponse(f"bomb-{compression_id}")
self.assertRaises(
IgnoreRequest,
mw.process_response,
response.request,
response,
spider,
)
def test_compression_bomb_spider_attr_br(self):
try:
import brotli # noqa: F401
except ImportError:
raise SkipTest("no brotli")
self._test_compression_bomb_spider_attr("br")
def test_compression_bomb_spider_attr_deflate(self):
self._test_compression_bomb_spider_attr("deflate")
def test_compression_bomb_spider_attr_gzip(self):
self._test_compression_bomb_spider_attr("gzip")
def test_compression_bomb_spider_attr_zstd(self):
self._test_compression_bomb_spider_attr("zstd")
def _test_compression_bomb_request_meta(self, compression_id):
crawler = get_crawler(Spider)
spider = crawler._create_spider("scrapytest.org")
mw = HttpCompressionMiddleware.from_crawler(crawler)
mw.open_spider(spider)
response = self._getresponse(f"bomb-{compression_id}")
response.meta["download_maxsize"] = 10_000_000
self.assertRaises(
IgnoreRequest,
mw.process_response,
response.request,
response,
spider,
)
def test_compression_bomb_request_meta_br(self):
try:
import brotli # noqa: F401
except ImportError:
raise SkipTest("no brotli")
self._test_compression_bomb_request_meta("br")
def test_compression_bomb_request_meta_deflate(self):
self._test_compression_bomb_request_meta("deflate")
def test_compression_bomb_request_meta_gzip(self):
self._test_compression_bomb_request_meta("gzip")
def test_compression_bomb_request_meta_zstd(self):
self._test_compression_bomb_request_meta("zstd")
class HttpCompressionSubclassTest(TestCase): class HttpCompressionSubclassTest(TestCase):

View File

@ -509,6 +509,7 @@ class SitemapSpiderTest(SpiderTest):
url="http://www.example.com/sitemap", url="http://www.example.com/sitemap",
body=self.GZBODY, body=self.GZBODY,
headers={"content-type": "application/gzip"}, headers={"content-type": "application/gzip"},
request=Request("http://www.example.com/sitemap"),
) )
self.assertSitemapBody(r, self.BODY) self.assertSitemapBody(r, self.BODY)
@ -517,7 +518,11 @@ class SitemapSpiderTest(SpiderTest):
self.assertSitemapBody(r, self.BODY) self.assertSitemapBody(r, self.BODY)
def test_get_sitemap_body_xml_url_compressed(self): def test_get_sitemap_body_xml_url_compressed(self):
r = Response(url="http://www.example.com/sitemap.xml.gz", body=self.GZBODY) r = Response(
url="http://www.example.com/sitemap.xml.gz",
body=self.GZBODY,
request=Request("http://www.example.com/sitemap"),
)
self.assertSitemapBody(r, self.BODY) self.assertSitemapBody(r, self.BODY)
# .xml.gz but body decoded by HttpCompression middleware already # .xml.gz but body decoded by HttpCompression middleware already
@ -694,13 +699,37 @@ Sitemap: /sitemap-relative-url.xml
["http://www.example.com/sitemap2.xml"], ["http://www.example.com/sitemap2.xml"],
) )
def test_compression_bomb(self): def test_compression_bomb_setting(self):
settings = {"DOWNLOAD_MAXSIZE": 10_000_000} settings = {"DOWNLOAD_MAXSIZE": 10_000_000}
crawler = get_crawler(settings_dict=settings) crawler = get_crawler(settings_dict=settings)
spider = self.spider_class.from_crawler(crawler, "example.com") spider = self.spider_class.from_crawler(crawler, "example.com")
body_path = Path(tests_datadir, "compressed", "bomb-gzip.bin") body_path = Path(tests_datadir, "compressed", "bomb-gzip.bin")
body = body_path.read_bytes() body = body_path.read_bytes()
response = Response(url="https://example.com", body=body) request = Request(url="https://example.com")
response = Response(url="https://example.com", body=body, request=request)
self.assertIsNone(spider._get_sitemap_body(response))
def test_compression_bomb_spider_attr(self):
class DownloadMaxSizeSpider(self.spider_class):
download_maxsize = 10_000_000
crawler = get_crawler()
spider = DownloadMaxSizeSpider.from_crawler(crawler, "example.com")
body_path = Path(tests_datadir, "compressed", "bomb-gzip.bin")
body = body_path.read_bytes()
request = Request(url="https://example.com")
response = Response(url="https://example.com", body=body, request=request)
self.assertIsNone(spider._get_sitemap_body(response))
def test_compression_bomb_request_meta(self):
crawler = get_crawler()
spider = self.spider_class.from_crawler(crawler, "example.com")
body_path = Path(tests_datadir, "compressed", "bomb-gzip.bin")
body = body_path.read_bytes()
request = Request(
url="https://example.com", meta={"download_maxsize": 10_000_000}
)
response = Response(url="https://example.com", body=body, request=request)
self.assertIsNone(spider._get_sitemap_body(response)) self.assertIsNone(spider._get_sitemap_body(response))