Skip to content

Commit e061bc6

Browse files
author
lukpueh
authored
Merge pull request #1202 from joshuagl/joshuagl/updater-simplify
Simplify updater logic for downloading and verifying target files
2 parents e005801 + 372e218 commit e061bc6

2 files changed

Lines changed: 33 additions & 192 deletions

File tree

tests/test_updater.py

Lines changed: 2 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1617,38 +1617,19 @@ def test_9__get_target_hash(self):
16171617

16181618

16191619

1620-
def test_10__hard_check_file_length(self):
1620+
def test_10__check_file_length(self):
16211621
# Test for exception if file object is not equal to trusted file length.
16221622
with tempfile.TemporaryFile() as temp_file_object:
16231623
temp_file_object.write(b'X')
16241624
temp_file_object.seek(0)
16251625
self.assertRaises(tuf.exceptions.DownloadLengthMismatchError,
1626-
self.repository_updater._hard_check_file_length,
1626+
self.repository_updater._check_file_length,
16271627
temp_file_object, 10)
16281628

16291629

16301630

16311631

16321632

1633-
def test_10__soft_check_file_length(self):
1634-
# Test for exception if file object is not equal to trusted file length.
1635-
with tempfile.TemporaryFile() as temp_file_object:
1636-
temp_file_object.write(b'XXX')
1637-
temp_file_object.seek(0)
1638-
self.assertRaises(tuf.exceptions.DownloadLengthMismatchError,
1639-
self.repository_updater._soft_check_file_length,
1640-
temp_file_object, 1)
1641-
1642-
# Verify that an exception is not raised if the file length <= the observed
1643-
# file length.
1644-
temp_file_object.seek(0)
1645-
self.repository_updater._soft_check_file_length(temp_file_object, 3)
1646-
temp_file_object.seek(0)
1647-
self.repository_updater._soft_check_file_length(temp_file_object, 4)
1648-
1649-
1650-
1651-
16521633
def test_10__targets_of_role(self):
16531634
# Test for non-existent role.
16541635
self.assertRaises(tuf.exceptions.UnknownRoleError,
@@ -1764,26 +1745,6 @@ def test_11__verify_metadata_file(self):
17641745
metadata_file_object, 'root')
17651746

17661747

1767-
def test_12__get_file(self):
1768-
# Test for an "unsafe" download, where the file is downloaded up to
1769-
# a required length (and no more). The "safe" download approach
1770-
# downloads an exact required length.
1771-
targets_path = os.path.join(self.repository_directory, 'metadata', 'targets.json')
1772-
1773-
file_size, file_hashes = securesystemslib.util.get_file_details(targets_path)
1774-
file_type = 'meta'
1775-
1776-
def verify_target_file(targets_path):
1777-
# Every target file must have its length and hashes inspected.
1778-
self.repository_updater._hard_check_file_length(targets_path, file_size)
1779-
self.repository_updater._check_hashes(targets_path, file_hashes)
1780-
1781-
self.repository_updater._get_file('targets.json', verify_target_file,
1782-
file_type, file_size, download_safely=True).close()
1783-
1784-
self.repository_updater._get_file('targets.json', verify_target_file,
1785-
file_type, file_size, download_safely=False).close()
1786-
17871748
def test_13__targets_of_role(self):
17881749
# Test case where a list of targets is given. By default, the 'targets'
17891750
# parameter is None.

tuf/client/updater.py

Lines changed: 31 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,7 +1206,7 @@ def _check_hashes(self, file_object, trusted_hashes):
12061206

12071207

12081208

1209-
def _hard_check_file_length(self, file_object, trusted_file_length):
1209+
def _check_file_length(self, file_object, trusted_file_length):
12101210
"""
12111211
<Purpose>
12121212
Non-public method that ensures the length of 'file_object' is strictly
@@ -1233,8 +1233,10 @@ def _hard_check_file_length(self, file_object, trusted_file_length):
12331233
None.
12341234
"""
12351235

1236-
file_object.seek(0)
1237-
observed_length = len(file_object.read())
1236+
# seek to the end of the file; that is offset 0 from the end of the file,
1237+
# represented by a whence value of 2
1238+
file_object.seek(0, 2)
1239+
observed_length = file_object.tell()
12381240

12391241
# Return and log a message if the length 'file_object' is equal to
12401242
# 'trusted_file_length', otherwise raise an exception. A hard check
@@ -1252,53 +1254,6 @@ def _hard_check_file_length(self, file_object, trusted_file_length):
12521254

12531255

12541256

1255-
def _soft_check_file_length(self, file_object, trusted_file_length):
1256-
"""
1257-
<Purpose>
1258-
Non-public method that checks the trusted file length of a file object.
1259-
The length of the file must be less than or equal to the expected
1260-
length. This is a deliberately redundant implementation designed to
1261-
complement tuf.download._check_downloaded_length().
1262-
1263-
<Arguments>
1264-
file_object:
1265-
A file object.
1266-
1267-
trusted_file_length:
1268-
A non-negative integer that is the trusted length of the file.
1269-
1270-
<Exceptions>
1271-
tuf.exceptions.DownloadLengthMismatchError, if the lengths do
1272-
not match.
1273-
1274-
<Side Effects>
1275-
Reads the contents of 'file_object' and logs a message if 'file_object'
1276-
is less than or equal to the trusted length.
1277-
Position within file_object is changed.
1278-
1279-
<Returns>
1280-
None.
1281-
"""
1282-
1283-
# Read the entire contents of 'file_object', a
1284-
file_object.seek(0)
1285-
observed_length = len(file_object.read())
1286-
1287-
# Return and log a message if 'file_object' is less than or equal to
1288-
# 'trusted_file_length', otherwise raise an exception. A soft check
1289-
# ensures that an upper bound restricts how large a file is downloaded.
1290-
if observed_length > trusted_file_length:
1291-
raise tuf.exceptions.DownloadLengthMismatchError(trusted_file_length,
1292-
observed_length)
1293-
1294-
else:
1295-
logger.debug('Observed length (' + str(observed_length) +\
1296-
') <= trusted length (' + str(trusted_file_length) + ')')
1297-
1298-
1299-
1300-
1301-
13021257
def _get_target_file(self, target_filepath, file_length, file_hashes,
13031258
prefix_filename_with_hash):
13041259
"""
@@ -1340,24 +1295,39 @@ def _get_target_file(self, target_filepath, file_length, file_hashes,
13401295
A file object containing the target.
13411296
"""
13421297

1343-
# Define a callable function that is passed as an argument to _get_file()
1344-
# and called. The 'verify_target_file' function ensures the file length
1345-
# and hashes of 'target_filepath' are strictly equal to the trusted values.
1346-
def verify_target_file(target_file_object):
1347-
1348-
# Every target file must have its length and hashes inspected.
1349-
self._hard_check_file_length(target_file_object, file_length)
1350-
self._check_hashes(target_file_object, file_hashes)
1351-
13521298
if self.consistent_snapshot and prefix_filename_with_hash:
13531299
# Note: values() does not return a list in Python 3. Use list()
13541300
# on values() for Python 2+3 compatibility.
13551301
target_digest = list(file_hashes.values()).pop()
13561302
dirname, basename = os.path.split(target_filepath)
13571303
target_filepath = os.path.join(dirname, target_digest + '.' + basename)
13581304

1359-
return self._get_file(target_filepath, verify_target_file,
1360-
'target', file_length, download_safely=True)
1305+
file_mirrors = tuf.mirrors.get_list_of_mirrors('target', target_filepath,
1306+
self.mirrors)
1307+
1308+
# file_mirror (URL): error (Exception)
1309+
file_mirror_errors = {}
1310+
file_object = None
1311+
1312+
for file_mirror in file_mirrors:
1313+
try:
1314+
file_object = tuf.download.safe_download(file_mirror, file_length)
1315+
1316+
# Verify 'file_object' against the expected length and hashes.
1317+
self._check_file_length(file_object, file_length)
1318+
self._check_hashes(file_object, file_hashes)
1319+
# If the file verifies, we don't need to try more mirrors
1320+
return file_object
1321+
1322+
except Exception as exception:
1323+
# Remember the error from this mirror, and "reset" the target file.
1324+
logger.debug('Update failed from ' + file_mirror + '.')
1325+
file_mirror_errors[file_mirror] = exception
1326+
file_object = None
1327+
1328+
logger.debug('Failed to update ' + repr(target_filepath) + ' from'
1329+
' all mirrors: ' + repr(file_mirror_errors))
1330+
raise tuf.exceptions.NoWorkingMirrorError(file_mirror_errors)
13611331

13621332

13631333

@@ -1641,96 +1611,6 @@ def _get_metadata_file(self, metadata_role, remote_filename,
16411611

16421612

16431613

1644-
def _get_file(self, filepath, verify_file_function, file_type, file_length,
1645-
download_safely=True):
1646-
"""
1647-
<Purpose>
1648-
Non-public method that tries downloading, up to a certain length, a
1649-
metadata or target file from a list of known mirrors. As soon as the first
1650-
valid copy of the file is found, the rest of the mirrors will be skipped.
1651-
1652-
<Arguments>
1653-
filepath:
1654-
The relative metadata or target filepath.
1655-
1656-
verify_file_function:
1657-
A callable function that expects a file object and raises an exception
1658-
if the file is invalid.
1659-
Target files and uncompressed versions of metadata may be verified with
1660-
'verify_file_function'.
1661-
1662-
file_type:
1663-
Type of data needed for download, must correspond to one of the strings
1664-
in the list ['meta', 'target']. 'meta' for metadata file type or
1665-
'target' for target file type. It should correspond to the
1666-
'securesystemslib.formats.NAME_SCHEMA' format.
1667-
1668-
file_length:
1669-
The expected length, or upper bound, of the target or metadata file to
1670-
be downloaded.
1671-
1672-
download_safely:
1673-
A boolean switch to toggle safe or unsafe download of the file.
1674-
1675-
<Exceptions>
1676-
tuf.exceptions.NoWorkingMirrorError:
1677-
The metadata could not be fetched. This is raised only when all known
1678-
mirrors failed to provide a valid copy of the desired metadata file.
1679-
1680-
<Side Effects>
1681-
The file is downloaded from all known repository mirrors in the worst
1682-
case. If a valid copy of the file is found, it is stored in a temporary
1683-
file and returned.
1684-
1685-
<Returns>
1686-
A file object containing the metadata or target.
1687-
"""
1688-
1689-
file_mirrors = tuf.mirrors.get_list_of_mirrors(file_type, filepath,
1690-
self.mirrors)
1691-
1692-
# file_mirror (URL): error (Exception)
1693-
file_mirror_errors = {}
1694-
file_object = None
1695-
1696-
for file_mirror in file_mirrors:
1697-
try:
1698-
# TODO: Instead of the more fragile 'download_safely' switch, unroll
1699-
# the function into two separate ones: one for "safe" download, and the
1700-
# other one for "unsafe" download? This should induce safer and more
1701-
# readable code.
1702-
if download_safely:
1703-
file_object = tuf.download.safe_download(file_mirror, file_length)
1704-
1705-
else:
1706-
file_object = tuf.download.unsafe_download(file_mirror, file_length)
1707-
1708-
# Verify 'file_object' according to the callable function.
1709-
# 'file_object' is also verified if decompressed above (i.e., the
1710-
# uncompressed version).
1711-
verify_file_function(file_object)
1712-
1713-
except Exception as exception:
1714-
# Remember the error from this mirror, and "reset" the target file.
1715-
logger.debug('Update failed from ' + file_mirror + '.')
1716-
file_mirror_errors[file_mirror] = exception
1717-
file_object = None
1718-
1719-
else:
1720-
break
1721-
1722-
if file_object:
1723-
return file_object
1724-
1725-
else:
1726-
logger.debug('Failed to update ' + repr(filepath) + ' from'
1727-
' all mirrors: ' + repr(file_mirror_errors))
1728-
raise tuf.exceptions.NoWorkingMirrorError(file_mirror_errors)
1729-
1730-
1731-
1732-
1733-
17341614
def _update_metadata(self, metadata_role, upperbound_filelength, version=None):
17351615
"""
17361616
<Purpose>

0 commit comments

Comments
 (0)