1
1
import aiohttp
2
2
import asyncio
3
+ import fnmatch
3
4
import json
4
5
import re
5
6
8
9
from logging import getLogger
9
10
from urllib import parse
10
11
12
+
11
13
from pulpcore .plugin .download import DownloaderFactory , HttpDownloader
12
14
13
- from pulp_container .constants import V2_ACCEPT_HEADERS
15
+ from pulp_container .constants import (
16
+ MANIFEST_MEDIA_TYPES ,
17
+ MANIFEST_PAYLOAD_MAX_SIZE ,
18
+ MEGABYTE ,
19
+ SIGNATURE_PAYLOAD_MAX_SIZE ,
20
+ V2_ACCEPT_HEADERS ,
21
+ )
22
+ from pulp_container .app .exceptions import InvalidRequest
23
+ from pulp_container .app .utils import resource_body_size_exceeded_msg
14
24
15
25
log = getLogger (__name__ )
16
26
17
27
HeadResult = namedtuple (
18
28
"HeadResult" ,
19
29
["status_code" , "path" , "artifact_attributes" , "url" , "headers" ],
20
30
)
31
+ DownloadResult = namedtuple ("DownloadResult" , ["url" , "artifact_attributes" , "path" , "headers" ])
32
+
21
33
34
+ class ValidateResourceSizeMixin :
35
+ async def _handle_response (self , response , content_type = None , max_body_size = None ):
36
+ """
37
+ Overrides the HttpDownloader method to be able to limit the request body size.
38
+ Handle the aiohttp response by writing it to disk and calculating digests
39
+ Args:
40
+ response (aiohttp.ClientResponse): The response to handle.
41
+ content_type (string): Type of the resource (manifest or signature) whose size
42
+ will be verified
43
+ max_body_size (int): Maximum allowed body size of the resource (manifest or signature).
44
+ Returns:
45
+ DownloadResult: Contains information about the result. See the DownloadResult docs for
46
+ more information.
47
+ """
48
+ if self .headers_ready_callback :
49
+ await self .headers_ready_callback (response .headers )
50
+ total_size = 0
51
+ while True :
52
+ chunk = await response .content .read (MEGABYTE )
53
+ total_size += len (chunk )
54
+ if max_body_size and total_size > max_body_size :
55
+ await self .finalize ()
56
+ raise InvalidRequest (resource_body_size_exceeded_msg (content_type , max_body_size ))
57
+ if not chunk :
58
+ await self .finalize ()
59
+ break # the download is done
60
+ await self .handle_data (chunk )
61
+ return DownloadResult (
62
+ path = self .path ,
63
+ artifact_attributes = self .artifact_attributes ,
64
+ url = self .url ,
65
+ headers = response .headers ,
66
+ )
22
67
23
- class RegistryAuthHttpDownloader (HttpDownloader ):
68
+ def get_content_type_and_max_resource_size (self , response ):
69
+ """
70
+ Returns the content_type (manifest or signature) based on the HTTP request and also the
71
+ corresponding resource allowed maximum size.
72
+ """
73
+ max_resource_size = None
74
+ content_type = response .content_type
75
+ is_cosign_tag = fnmatch .fnmatch (response .url .name , "sha256-*.sig" )
76
+ if isinstance (self , NoAuthSignatureDownloader ) or is_cosign_tag :
77
+ max_resource_size = SIGNATURE_PAYLOAD_MAX_SIZE
78
+ content_type = "Signature"
79
+ elif content_type in MANIFEST_MEDIA_TYPES .IMAGE + MANIFEST_MEDIA_TYPES .LIST :
80
+ max_resource_size = MANIFEST_PAYLOAD_MAX_SIZE
81
+ content_type = "Manifest"
82
+ return content_type , max_resource_size
83
+
84
+
85
+ class RegistryAuthHttpDownloader (ValidateResourceSizeMixin , HttpDownloader ):
24
86
"""
25
87
Custom Downloader that automatically handles Token Based and Basic Authentication.
26
88
@@ -104,7 +166,10 @@ async def _run(self, handle_401=True, extra_data=None):
104
166
if http_method == "head" :
105
167
to_return = await self ._handle_head_response (response )
106
168
else :
107
- to_return = await self ._handle_response (response )
169
+ content_type , max_resource_size = self .get_content_type_and_max_resource_size (
170
+ response
171
+ )
172
+ to_return = await self ._handle_response (response , content_type , max_resource_size )
108
173
109
174
await response .release ()
110
175
self .response_headers = response .headers
@@ -193,7 +258,7 @@ async def _handle_head_response(self, response):
193
258
)
194
259
195
260
196
- class NoAuthSignatureDownloader (HttpDownloader ):
261
+ class NoAuthSignatureDownloader (ValidateResourceSizeMixin , HttpDownloader ):
197
262
"""A downloader class suited for signature downloads."""
198
263
199
264
def raise_for_status (self , response ):
@@ -208,6 +273,20 @@ def raise_for_status(self, response):
208
273
else :
209
274
response .raise_for_status ()
210
275
276
+ async def _run (self , extra_data = None ):
277
+ if self .download_throttler :
278
+ await self .download_throttler .acquire ()
279
+ async with self .session .get (
280
+ self .url , proxy = self .proxy , proxy_auth = self .proxy_auth , auth = self .auth
281
+ ) as response :
282
+ self .raise_for_status (response )
283
+ content_type , max_resource_size = self .get_content_type_and_max_resource_size (response )
284
+ to_return = await self ._handle_response (response , content_type , max_resource_size )
285
+ await response .release ()
286
+ if self ._close_session_on_finalize :
287
+ await self .session .close ()
288
+ return to_return
289
+
211
290
212
291
class NoAuthDownloaderFactory (DownloaderFactory ):
213
292
"""
0 commit comments