Skip to content
This repository has been archived by the owner on Oct 2, 2024. It is now read-only.

Return error if blob download is incomplete by validating hash digest. #1833

Merged
merged 8 commits into from
Feb 21, 2024
21 changes: 14 additions & 7 deletions lib/registry.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import getpass
import hashlib
import io
import os
import re
Expand Down Expand Up @@ -387,17 +388,21 @@ def blob_exists_p(self, digest):
# requests.head() does not follow redirects, and it seems like a
# weird status, so I worry there is a gotcha I haven’t figured out.
url = self._url_of("blobs", "sha256:%s" % digest)
res = self.request("HEAD", url, {200,401,404})
res, m = self.request("HEAD", url, {200,401,404})
return (res.status_code == 200)

def blob_to_file(self, digest, path, msg):
"GET the blob with hash digest and save it at path."
# /v2/library/hello-world/blobs/<layer-hash>
url = self._url_of("blobs", "sha256:" + digest)
sw = ch.Progress_Writer(path, msg)
self.request("GET", url, out=sw)
res, m = self.request("GET", url, out=sw)
sw.close()

# Check for integrity of downloaded blob by validating its hash digest
if (str(digest) != str(m.hexdigest())):
kchilleri marked this conversation as resolved.
Show resolved Hide resolved
ch.FATAL("Incomplete blob download.")
kchilleri marked this conversation as resolved.
Show resolved Hide resolved

def blob_upload(self, digest, data, note=""):
"""Upload blob with hash digest to url. data is the data to upload, and
can be anything requests can handle; if it’s an open file, then it’s
Expand All @@ -416,13 +421,13 @@ def blob_upload(self, digest, data, note=""):
ch.INFO(msg)
# 2. Get upload URL for blob.
url = self._url_of("blobs", "uploads/")
res = self.request("POST", url, {202})
res, m = self.request("POST", url, {202})
# 3. Upload blob. We do a “monolithic” upload (i.e., send all the
# content in a single PUT request) as opposed to a “chunked” upload
# (i.e., send data in multiple PATCH requests followed by a PUT request
# with no body).
url = res.headers["Location"]
res = self.request("PUT", url, {201}, data=data,
res, m = self.request("PUT", url, {201}, data=data,
params={ "digest": "sha256:%s" % digest })
if (isinstance(data, ch.Progress_Reader)):
data.close()
Expand Down Expand Up @@ -471,7 +476,7 @@ def fatman_to_file(self, path, msg):
# when trying to create schema1 manifest”.
accept = "%s;q=0.5" % ",".join( list(TYPES_INDEX.values())
+ list(TYPES_MANIFEST.values()))
res = self.request("GET", url, out=pw, statuses={200, 401, 404, 429},
res, m = self.request("GET", url, out=pw, statuses={200, 401, 404, 429},
headers={ "Accept" : accept })
pw.close()
if (res.status_code == 429):
Expand Down Expand Up @@ -503,7 +508,7 @@ def manifest_to_file(self, path, msg, digest=None):
url = self._url_of("manifests", digest)
pw = ch.Progress_Writer(path, msg)
accept = "%s;q=0.5" % ",".join(TYPES_MANIFEST.values())
res = self.request("GET", url, out=pw, statuses={200, 401, 404},
res, m = self.request("GET", url, out=pw, statuses={200, 401, 404},
headers={ "Accept" : accept })
pw.close()
if (res.status_code != 200):
Expand Down Expand Up @@ -547,6 +552,7 @@ def request(self, method, url, statuses={200}, out=None, **kwargs):
else:
ch.FATAL("unhandled authentication failure")
# Stream response if needed.
m = hashlib.sha256() # store downloaded hash digest to check for blob download integrity
if (out is not None and res.status_code == 200):
try:
length = int(res.headers["Content-Length"])
Expand All @@ -557,8 +563,9 @@ def request(self, method, url, statuses={200}, out=None, **kwargs):
out.start(length)
for chunk in res.iter_content(ch.HTTP_CHUNK_SIZE):
out.write(chunk)
m.update(chunk)
# Done.
return res
return res, m
kchilleri marked this conversation as resolved.
Show resolved Hide resolved

def request_raw(self, method, url, statuses, auth=None, **kwargs):
"""Request url using method. statuses is an iterable of acceptable
Expand Down
Loading