Skip to content

Commit 35587c4

Browse files
committed
Move download modules inside client directory
The modules performing network download are used only by the client side of TUF. Move them inside the client directory for the refactored client. Move the _mirror_*download functions from Updater to mirrors.py. Signed-off-by: Teodora Sechkova <tsechkova@vmware.com>
1 parent ad30da8 commit 35587c4

File tree

5 files changed

+750
-61
lines changed

5 files changed

+750
-61
lines changed

tuf/client_rework/download.py

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
#!/usr/bin/env python
2+
3+
# Copyright 2012 - 2017, New York University and the TUF contributors
4+
# SPDX-License-Identifier: MIT OR Apache-2.0
5+
6+
"""
7+
<Program Name>
8+
download.py
9+
10+
<Started>
11+
February 21, 2012. Based on previous version by Geremy Condra.
12+
13+
<Author>
14+
Konstantin Andrianov
15+
Vladimir Diaz <vladimir.v.diaz@gmail.com>
16+
17+
<Copyright>
18+
See LICENSE-MIT OR LICENSE for licensing information.
19+
20+
<Purpose>
21+
Download metadata and target files and check their validity. The hash and
22+
length of a downloaded file has to match the hash and length supplied by the
23+
metadata of that file.
24+
"""
25+
26+
# Help with Python 3 compatibility, where the print statement is a function, an
27+
# implicit relative import is invalid, and the '/' operator performs true
28+
# division. Example: print 'hello world' raises a 'SyntaxError' exception.
29+
from __future__ import print_function
30+
from __future__ import absolute_import
31+
from __future__ import division
32+
from __future__ import unicode_literals
33+
34+
import logging
35+
import timeit
36+
import tempfile
37+
38+
import securesystemslib
39+
import securesystemslib.util
40+
import six
41+
42+
import tuf
43+
import tuf.exceptions
44+
import tuf.formats
45+
46+
# See 'log.py' to learn how logging is handled in TUF.
47+
logger = logging.getLogger(__name__)
48+
49+
50+
def safe_download(url, required_length, fetcher):
51+
"""
52+
<Purpose>
53+
Given the 'url' and 'required_length' of the desired file, open a connection
54+
to 'url', download it, and return the contents of the file. Also ensure
55+
the length of the downloaded file matches 'required_length' exactly.
56+
tuf.download.unsafe_download() may be called if an upper download limit is
57+
preferred.
58+
59+
<Arguments>
60+
url:
61+
A URL string that represents the location of the file.
62+
63+
required_length:
64+
An integer value representing the length of the file. This is an exact
65+
limit.
66+
67+
fetcher:
68+
An object implementing FetcherInterface that performs the network IO
69+
operations.
70+
71+
<Side Effects>
72+
A file object is created on disk to store the contents of 'url'.
73+
74+
<Exceptions>
75+
tuf.ssl_commons.exceptions.DownloadLengthMismatchError, if there was a
76+
mismatch of observed vs expected lengths while downloading the file.
77+
78+
securesystemslib.exceptions.FormatError, if any of the arguments are
79+
improperly formatted.
80+
81+
Any other unforeseen runtime exception.
82+
83+
<Returns>
84+
A file object that points to the contents of 'url'.
85+
"""
86+
87+
# Do all of the arguments have the appropriate format?
88+
# Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch.
89+
securesystemslib.formats.URL_SCHEMA.check_match(url)
90+
tuf.formats.LENGTH_SCHEMA.check_match(required_length)
91+
92+
return _download_file(url, required_length, fetcher, STRICT_REQUIRED_LENGTH=True)
93+
94+
95+
96+
97+
98+
def unsafe_download(url, required_length, fetcher):
99+
"""
100+
<Purpose>
101+
Given the 'url' and 'required_length' of the desired file, open a connection
102+
to 'url', download it, and return the contents of the file. Also ensure
103+
the length of the downloaded file is up to 'required_length', and no larger.
104+
tuf.download.safe_download() may be called if an exact download limit is
105+
preferred.
106+
107+
<Arguments>
108+
url:
109+
A URL string that represents the location of the file.
110+
111+
required_length:
112+
An integer value representing the length of the file. This is an upper
113+
limit.
114+
115+
fetcher:
116+
An object implementing FetcherInterface that performs the network IO
117+
operations.
118+
119+
<Side Effects>
120+
A file object is created on disk to store the contents of 'url'.
121+
122+
<Exceptions>
123+
tuf.ssl_commons.exceptions.DownloadLengthMismatchError, if there was a
124+
mismatch of observed vs expected lengths while downloading the file.
125+
126+
securesystemslib.exceptions.FormatError, if any of the arguments are
127+
improperly formatted.
128+
129+
Any other unforeseen runtime exception.
130+
131+
<Returns>
132+
A file object that points to the contents of 'url'.
133+
"""
134+
135+
# Do all of the arguments have the appropriate format?
136+
# Raise 'securesystemslib.exceptions.FormatError' if there is a mismatch.
137+
securesystemslib.formats.URL_SCHEMA.check_match(url)
138+
tuf.formats.LENGTH_SCHEMA.check_match(required_length)
139+
140+
return _download_file(url, required_length, fetcher, STRICT_REQUIRED_LENGTH=False)
141+
142+
143+
144+
145+
146+
def _download_file(url, required_length, fetcher, STRICT_REQUIRED_LENGTH=True):
147+
"""
148+
<Purpose>
149+
Given the url and length of the desired file, this function opens a
150+
connection to 'url' and downloads the file while ensuring its length
151+
matches 'required_length' if 'STRICT_REQUIRED_LENGH' is True (If False,
152+
the file's length is not checked and a slow retrieval exception is raised
153+
if the downloaded rate falls below the acceptable rate).
154+
155+
<Arguments>
156+
url:
157+
A URL string that represents the location of the file.
158+
159+
required_length:
160+
An integer value representing the length of the file.
161+
162+
STRICT_REQUIRED_LENGTH:
163+
A Boolean indicator used to signal whether we should perform strict
164+
checking of required_length. True by default. We explicitly set this to
165+
False when we know that we want to turn this off for downloading the
166+
timestamp metadata, which has no signed required_length.
167+
168+
<Side Effects>
169+
A file object is created on disk to store the contents of 'url'.
170+
171+
<Exceptions>
172+
tuf.exceptions.DownloadLengthMismatchError, if there was a
173+
mismatch of observed vs expected lengths while downloading the file.
174+
175+
securesystemslib.exceptions.FormatError, if any of the arguments are
176+
improperly formatted.
177+
178+
Any other unforeseen runtime exception.
179+
180+
<Returns>
181+
A file object that points to the contents of 'url'.
182+
"""
183+
# 'url.replace('\\', '/')' is needed for compatibility with Windows-based
184+
# systems, because they might use back-slashes in place of forward-slashes.
185+
# This converts it to the common format. unquote() replaces %xx escapes in a
186+
# url with their single-character equivalent. A back-slash may be encoded as
187+
# %5c in the url, which should also be replaced with a forward slash.
188+
url = six.moves.urllib.parse.unquote(url).replace('\\', '/')
189+
logger.info('Downloading: ' + repr(url))
190+
191+
# This is the temporary file that we will return to contain the contents of
192+
# the downloaded file.
193+
temp_file = tempfile.TemporaryFile()
194+
195+
average_download_speed = 0
196+
number_of_bytes_received = 0
197+
198+
try:
199+
chunks = fetcher.fetch(url, required_length)
200+
start_time = timeit.default_timer()
201+
for chunk in chunks:
202+
203+
stop_time = timeit.default_timer()
204+
temp_file.write(chunk)
205+
206+
# Measure the average download speed.
207+
number_of_bytes_received += len(chunk)
208+
seconds_spent_receiving = stop_time - start_time
209+
average_download_speed = number_of_bytes_received / seconds_spent_receiving
210+
211+
if average_download_speed < tuf.settings.MIN_AVERAGE_DOWNLOAD_SPEED:
212+
logger.debug('The average download speed dropped below the minimum'
213+
' average download speed set in tuf.settings.py. Stopping the'
214+
' download!')
215+
break
216+
217+
else:
218+
logger.debug('The average download speed has not dipped below the'
219+
' minimum average download speed set in tuf.settings.py.')
220+
221+
# Does the total number of downloaded bytes match the required length?
222+
_check_downloaded_length(number_of_bytes_received, required_length,
223+
STRICT_REQUIRED_LENGTH=STRICT_REQUIRED_LENGTH,
224+
average_download_speed=average_download_speed)
225+
226+
except Exception:
227+
# Close 'temp_file'. Any written data is lost.
228+
temp_file.close()
229+
logger.debug('Could not download URL: ' + repr(url))
230+
raise
231+
232+
else:
233+
return temp_file
234+
235+
236+
237+
238+
def _check_downloaded_length(total_downloaded, required_length,
239+
STRICT_REQUIRED_LENGTH=True,
240+
average_download_speed=None):
241+
"""
242+
<Purpose>
243+
A helper function which checks whether the total number of downloaded bytes
244+
matches our expectation.
245+
246+
<Arguments>
247+
total_downloaded:
248+
The total number of bytes supposedly downloaded for the file in question.
249+
250+
required_length:
251+
The total number of bytes expected of the file as seen from its metadata.
252+
The Timestamp role is always downloaded without a known file length, and
253+
the Root role when the client cannot download any of the required
254+
top-level roles. In both cases, 'required_length' is actually an upper
255+
limit on the length of the downloaded file.
256+
257+
STRICT_REQUIRED_LENGTH:
258+
A Boolean indicator used to signal whether we should perform strict
259+
checking of required_length. True by default. We explicitly set this to
260+
False when we know that we want to turn this off for downloading the
261+
timestamp metadata, which has no signed required_length.
262+
263+
average_download_speed:
264+
The average download speed for the downloaded file.
265+
266+
<Side Effects>
267+
None.
268+
269+
<Exceptions>
270+
securesystemslib.exceptions.DownloadLengthMismatchError, if
271+
STRICT_REQUIRED_LENGTH is True and total_downloaded is not equal
272+
required_length.
273+
274+
tuf.exceptions.SlowRetrievalError, if the total downloaded was
275+
done in less than the acceptable download speed (as set in
276+
tuf.settings.py).
277+
278+
<Returns>
279+
None.
280+
"""
281+
282+
if total_downloaded == required_length:
283+
logger.info('Downloaded ' + str(total_downloaded) + ' bytes out of the'
284+
' expected ' + str(required_length) + ' bytes.')
285+
286+
else:
287+
difference_in_bytes = abs(total_downloaded - required_length)
288+
289+
# What we downloaded is not equal to the required length, but did we ask
290+
# for strict checking of required length?
291+
if STRICT_REQUIRED_LENGTH:
292+
logger.info('Downloaded ' + str(total_downloaded) + ' bytes, but'
293+
' expected ' + str(required_length) + ' bytes. There is a difference'
294+
' of ' + str(difference_in_bytes) + ' bytes.')
295+
296+
# If the average download speed is below a certain threshold, we flag
297+
# this as a possible slow-retrieval attack.
298+
logger.debug('Average download speed: ' + repr(average_download_speed))
299+
logger.debug('Minimum average download speed: ' + repr(tuf.settings.MIN_AVERAGE_DOWNLOAD_SPEED))
300+
301+
if average_download_speed < tuf.settings.MIN_AVERAGE_DOWNLOAD_SPEED:
302+
raise tuf.exceptions.SlowRetrievalError(average_download_speed)
303+
304+
else:
305+
logger.debug('Good average download speed: ' +
306+
repr(average_download_speed) + ' bytes per second')
307+
308+
raise tuf.exceptions.DownloadLengthMismatchError(required_length, total_downloaded)
309+
310+
else:
311+
# We specifically disabled strict checking of required length, but we
312+
# will log a warning anyway. This is useful when we wish to download the
313+
# Timestamp or Root metadata, for which we have no signed metadata; so,
314+
# we must guess a reasonable required_length for it.
315+
if average_download_speed < tuf.settings.MIN_AVERAGE_DOWNLOAD_SPEED:
316+
raise tuf.exceptions.SlowRetrievalError(average_download_speed)
317+
318+
else:
319+
logger.debug('Good average download speed: ' +
320+
repr(average_download_speed) + ' bytes per second')
321+
322+
logger.info('Downloaded ' + str(total_downloaded) + ' bytes out of an'
323+
' upper limit of ' + str(required_length) + ' bytes.')

tuf/client_rework/fetcher.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Copyright 2021, New York University and the TUF contributors
2+
# SPDX-License-Identifier: MIT OR Apache-2.0
3+
4+
"""Provides an interface for network IO abstraction.
5+
"""
6+
7+
# Imports
8+
import abc
9+
10+
# Classes
11+
class FetcherInterface():
12+
"""Defines an interface for abstract network download.
13+
14+
By providing a concrete implementation of the abstract interface,
15+
users of the framework can plug-in their preferred/customized
16+
network stack.
17+
"""
18+
19+
__metaclass__ = abc.ABCMeta
20+
21+
@abc.abstractmethod
22+
def fetch(self, url, required_length):
23+
"""Fetches the contents of HTTP/HTTPS url from a remote server.
24+
25+
Ensures the length of the downloaded data is up to 'required_length'.
26+
27+
Arguments:
28+
url: A URL string that represents a file location.
29+
required_length: An integer value representing the file length in bytes.
30+
31+
Raises:
32+
tuf.exceptions.SlowRetrievalError: A timeout occurs while receiving data.
33+
tuf.exceptions.FetcherHTTPError: An HTTP error code is received.
34+
35+
Returns:
36+
A bytes iterator
37+
"""
38+
raise NotImplementedError # pragma: no cover

0 commit comments

Comments
 (0)