Skip to content

Commit ab1520e

Browse files
author
Jussi Kukkonen
authored
Merge pull request #1250 from sechkova/fetcher
Abstract out the network IO
2 parents 8308f85 + 93c6573 commit ab1520e

20 files changed

+520
-260
lines changed

tests/test_arbitrary_package_attack.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ def setUp(self):
124124
# Set the url prefix required by the 'tuf/client/updater.py' updater.
125125
# 'path/to/tmp/repository' -> 'localhost:8001/tmp/repository'.
126126
repository_basepath = self.repository_directory[len(os.getcwd()):]
127-
url_prefix = 'http://localhost:' \
127+
url_prefix = 'http://' + utils.TEST_HOST_ADDRESS + ':' \
128128
+ str(self.server_process_handler.port) + repository_basepath
129129

130130
# Setting 'tuf.settings.repository_directory' with the temporary client

tests/test_download.py

Lines changed: 62 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343

4444
import tuf
4545
import tuf.download as download
46+
import tuf.requests_fetcher
4647
import tuf.log
4748
import tuf.unittest_toolbox as unittest_toolbox
4849
import tuf.exceptions
@@ -76,7 +77,7 @@ def setUp(self):
7677
self.server_process_handler = utils.TestServerProcess(log=logger)
7778

7879
rel_target_filepath = os.path.basename(target_filepath)
79-
self.url = 'http://localhost:' \
80+
self.url = 'http://' + utils.TEST_HOST_ADDRESS + ':' \
8081
+ str(self.server_process_handler.port) + '/' + rel_target_filepath
8182

8283
# Computing hash of target file data.
@@ -85,6 +86,10 @@ def setUp(self):
8586
digest = m.hexdigest()
8687
self.target_hash = {'md5':digest}
8788

89+
# Initialize the default fetcher for the download
90+
self.fetcher = tuf.requests_fetcher.RequestsFetcher()
91+
92+
8893

8994
# Stop server process and perform clean up.
9095
def tearDown(self):
@@ -100,13 +105,36 @@ def tearDown(self):
100105
def test_download_url_to_tempfileobj(self):
101106

102107
download_file = download.safe_download
103-
with download_file(self.url, self.target_data_length) as temp_fileobj:
108+
with download_file(self.url, self.target_data_length, self.fetcher) as temp_fileobj:
104109
temp_fileobj.seek(0)
105110
temp_file_data = temp_fileobj.read().decode('utf-8')
106111
self.assertEqual(self.target_data, temp_file_data)
107112
self.assertEqual(self.target_data_length, len(temp_file_data))
108113

109114

115+
# Test: Download url in more than one chunk.
116+
def test_download_url_in_chunks(self):
117+
118+
# Set smaller chunk size to ensure that the file will be downloaded
119+
# in more than one chunk
120+
default_chunk_size = tuf.settings.CHUNK_SIZE
121+
tuf.settings.CHUNK_SIZE = 4
122+
# We don't have access to chunks from download_file()
123+
# so we just confirm that the expectation of more than one chunk is
124+
# correct and verify that no errors are raised during download
125+
chunks_count = self.target_data_length/tuf.settings.CHUNK_SIZE
126+
self.assertGreater(chunks_count, 1)
127+
128+
download_file = download.safe_download
129+
with download_file(self.url, self.target_data_length, self.fetcher) as temp_fileobj:
130+
temp_fileobj.seek(0)
131+
temp_file_data = temp_fileobj.read().decode('utf-8')
132+
self.assertEqual(self.target_data, temp_file_data)
133+
self.assertEqual(self.target_data_length, len(temp_file_data))
134+
135+
# Restore default settings
136+
tuf.settings.CHUNK_SIZE = default_chunk_size
137+
110138

111139
# Test: Incorrect lengths.
112140
def test_download_url_to_tempfileobj_and_lengths(self):
@@ -118,18 +146,18 @@ def test_download_url_to_tempfileobj_and_lengths(self):
118146
# the server-reported length of the file does not match the
119147
# required_length. 'updater.py' *does* verify the hashes of downloaded
120148
# content.
121-
download.safe_download(self.url, self.target_data_length - 4).close()
122-
download.unsafe_download(self.url, self.target_data_length - 4).close()
149+
download.safe_download(self.url, self.target_data_length - 4, self.fetcher).close()
150+
download.unsafe_download(self.url, self.target_data_length - 4, self.fetcher).close()
123151

124152
# We catch 'tuf.exceptions.DownloadLengthMismatchError' for safe_download()
125153
# because it will not download more bytes than requested (in this case, a
126154
# length greater than the size of the target file).
127155
self.assertRaises(tuf.exceptions.DownloadLengthMismatchError,
128-
download.safe_download, self.url, self.target_data_length + 1)
156+
download.safe_download, self.url, self.target_data_length + 1, self.fetcher)
129157

130158
# Calling unsafe_download() with a mismatched length should not raise an
131159
# exception.
132-
download.unsafe_download(self.url, self.target_data_length + 1).close()
160+
download.unsafe_download(self.url, self.target_data_length + 1, self.fetcher).close()
133161

134162

135163

@@ -164,32 +192,26 @@ def test_download_url_to_tempfileobj_and_urls(self):
164192
download_file = download.safe_download
165193
unsafe_download_file = download.unsafe_download
166194

167-
self.assertRaises(securesystemslib.exceptions.FormatError,
168-
download_file, None, self.target_data_length)
169-
170-
self.assertRaises(tuf.exceptions.URLParsingError,
171-
download_file,
172-
self.random_string(), self.target_data_length)
195+
with self.assertRaises(securesystemslib.exceptions.FormatError):
196+
download_file(None, self.target_data_length, self.fetcher)
173197

174-
url = 'http://localhost:' \
198+
url = 'http://' + utils.TEST_HOST_ADDRESS + ':' \
175199
+ str(self.server_process_handler.port) + '/' + self.random_string()
176-
self.assertRaises(requests.exceptions.HTTPError,
177-
download_file,
178-
url,
179-
self.target_data_length)
180-
url1 = 'http://localhost:' \
200+
with self.assertRaises(tuf.exceptions.FetcherHTTPError) as cm:
201+
download_file(url, self.target_data_length, self.fetcher)
202+
self.assertEqual(cm.exception.status_code, 404)
203+
204+
url1 = 'http://' + utils.TEST_HOST_ADDRESS + ':' \
181205
+ str(self.server_process_handler.port + 1) + '/' + self.random_string()
182-
self.assertRaises(requests.exceptions.ConnectionError,
183-
download_file,
184-
url1,
185-
self.target_data_length)
206+
with self.assertRaises(requests.exceptions.ConnectionError):
207+
download_file(url1, self.target_data_length, self.fetcher)
186208

187209
# Specify an unsupported URI scheme.
188210
url_with_unsupported_uri = self.url.replace('http', 'file')
189211
self.assertRaises(requests.exceptions.InvalidSchema, download_file, url_with_unsupported_uri,
190-
self.target_data_length)
212+
self.target_data_length, self.fetcher)
191213
self.assertRaises(requests.exceptions.InvalidSchema, unsafe_download_file,
192-
url_with_unsupported_uri, self.target_data_length)
214+
url_with_unsupported_uri, self.target_data_length, self.fetcher)
193215

194216

195217

@@ -290,7 +312,7 @@ def test_https_connection(self):
290312
os.environ['REQUESTS_CA_BUNDLE'] = bad_cert_fname
291313
# Clear sessions to ensure that the certificate we just specified is used.
292314
# TODO: Confirm necessity of this session clearing and lay out mechanics.
293-
tuf.download._sessions = {}
315+
self.fetcher._sessions = {}
294316

295317
# Try connecting to the server process with the bad cert while trusting
296318
# the bad cert. Expect failure because even though we trust it, the
@@ -302,41 +324,41 @@ def test_https_connection(self):
302324
category=urllib3.exceptions.SubjectAltNameWarning)
303325

304326
with self.assertRaises(requests.exceptions.SSLError):
305-
download.safe_download(bad_https_url, target_data_length)
327+
download.safe_download(bad_https_url, target_data_length, self.fetcher)
306328
with self.assertRaises(requests.exceptions.SSLError):
307-
download.unsafe_download(bad_https_url, target_data_length)
329+
download.unsafe_download(bad_https_url, target_data_length, self.fetcher)
308330

309331
# Try connecting to the server processes with the good certs while not
310332
# trusting the good certs (trusting the bad cert instead). Expect failure
311333
# because even though the server's cert file is otherwise OK, we don't
312334
# trust it.
313335
logger.info('Trying HTTPS download of target file: ' + good_https_url)
314336
with self.assertRaises(requests.exceptions.SSLError):
315-
download.safe_download(good_https_url, target_data_length)
337+
download.safe_download(good_https_url, target_data_length, self.fetcher)
316338
with self.assertRaises(requests.exceptions.SSLError):
317-
download.unsafe_download(good_https_url, target_data_length)
339+
download.unsafe_download(good_https_url, target_data_length, self.fetcher)
318340

319341
logger.info('Trying HTTPS download of target file: ' + good2_https_url)
320342
with self.assertRaises(requests.exceptions.SSLError):
321-
download.safe_download(good2_https_url, target_data_length)
343+
download.safe_download(good2_https_url, target_data_length, self.fetcher)
322344
with self.assertRaises(requests.exceptions.SSLError):
323-
download.unsafe_download(good2_https_url, target_data_length)
345+
download.unsafe_download(good2_https_url, target_data_length, self.fetcher)
324346

325347

326348
# Configure environment to now trust the certfile that is expired.
327349
os.environ['REQUESTS_CA_BUNDLE'] = expired_cert_fname
328350
# Clear sessions to ensure that the certificate we just specified is used.
329351
# TODO: Confirm necessity of this session clearing and lay out mechanics.
330-
tuf.download._sessions = {}
352+
self.fetcher._sessions = {}
331353

332354
# Try connecting to the server process with the expired cert while
333355
# trusting the expired cert. Expect failure because even though we trust
334356
# it, it is expired.
335357
logger.info('Trying HTTPS download of target file: ' + expired_https_url)
336358
with self.assertRaises(requests.exceptions.SSLError):
337-
download.safe_download(expired_https_url, target_data_length)
359+
download.safe_download(expired_https_url, target_data_length, self.fetcher)
338360
with self.assertRaises(requests.exceptions.SSLError):
339-
download.unsafe_download(expired_https_url, target_data_length)
361+
download.unsafe_download(expired_https_url, target_data_length, self.fetcher)
340362

341363

342364
# Try connecting to the server processes with the good certs while
@@ -346,18 +368,18 @@ def test_https_connection(self):
346368
os.environ['REQUESTS_CA_BUNDLE'] = good_cert_fname
347369
# Clear sessions to ensure that the certificate we just specified is used.
348370
# TODO: Confirm necessity of this session clearing and lay out mechanics.
349-
tuf.download._sessions = {}
371+
self.fetcher._sessions = {}
350372
logger.info('Trying HTTPS download of target file: ' + good_https_url)
351-
download.safe_download(good_https_url, target_data_length).close()
352-
download.unsafe_download(good_https_url, target_data_length).close()
373+
download.safe_download(good_https_url, target_data_length, self.fetcher).close()
374+
download.unsafe_download(good_https_url, target_data_length,self.fetcher).close()
353375

354376
os.environ['REQUESTS_CA_BUNDLE'] = good2_cert_fname
355377
# Clear sessions to ensure that the certificate we just specified is used.
356378
# TODO: Confirm necessity of this session clearing and lay out mechanics.
357-
tuf.download._sessions = {}
379+
self.fetcher._sessions = {}
358380
logger.info('Trying HTTPS download of target file: ' + good2_https_url)
359-
download.safe_download(good2_https_url, target_data_length).close()
360-
download.unsafe_download(good2_https_url, target_data_length).close()
381+
download.safe_download(good2_https_url, target_data_length, self.fetcher).close()
382+
download.unsafe_download(good2_https_url, target_data_length, self.fetcher).close()
361383

362384
finally:
363385
for proc_handler in [

tests/test_endless_data_attack.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ def setUp(self):
126126
# Set the url prefix required by the 'tuf/client/updater.py' updater.
127127
# 'path/to/tmp/repository' -> 'localhost:8001/tmp/repository'.
128128
repository_basepath = self.repository_directory[len(os.getcwd()):]
129-
url_prefix = 'http://localhost:' \
129+
url_prefix = 'http://' + utils.TEST_HOST_ADDRESS + ':' \
130130
+ str(self.server_process_handler.port) + repository_basepath
131131

132132
# Setting 'tuf.settings.repository_directory' with the temporary client

tests/test_extraneous_dependencies_attack.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def setUp(self):
133133
# Set the url prefix required by the 'tuf/client/updater.py' updater.
134134
# 'path/to/tmp/repository' -> 'localhost:8001/tmp/repository'.
135135
repository_basepath = self.repository_directory[len(os.getcwd()):]
136-
url_prefix = 'http://localhost:' \
136+
url_prefix = 'http://' + utils.TEST_HOST_ADDRESS + ':' \
137137
+ str(self.server_process_handler.port) + repository_basepath
138138

139139
# Setting 'tuf.settings.repository_directory' with the temporary client

tests/test_fetcher.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
#!/usr/bin/env python
2+
3+
# Copyright 2021, New York University and the TUF contributors
4+
# SPDX-License-Identifier: MIT OR Apache-2.0
5+
6+
"""Unit test for RequestsFetcher.
7+
"""
8+
# Help with Python 2 compatibility, where the '/' operator performs
9+
# integer division.
10+
from __future__ import division
11+
12+
import logging
13+
import os
14+
import io
15+
import sys
16+
import unittest
17+
import tempfile
18+
import math
19+
20+
import tuf
21+
import tuf.exceptions
22+
import tuf.requests_fetcher
23+
import tuf.unittest_toolbox as unittest_toolbox
24+
25+
from tests import utils
26+
27+
logger = logging.getLogger(__name__)
28+
29+
30+
class TestFetcher(unittest_toolbox.Modified_TestCase):
31+
def setUp(self):
32+
"""
33+
Create a temporary file and launch a simple server in the
34+
current working directory.
35+
"""
36+
37+
unittest_toolbox.Modified_TestCase.setUp(self)
38+
39+
# Making a temporary file.
40+
current_dir = os.getcwd()
41+
target_filepath = self.make_temp_data_file(directory=current_dir)
42+
self.target_fileobj = open(target_filepath, 'r')
43+
self.file_contents = self.target_fileobj.read()
44+
self.file_length = len(self.file_contents)
45+
46+
# Launch a SimpleHTTPServer (serves files in the current dir).
47+
self.server_process_handler = utils.TestServerProcess(log=logger)
48+
49+
rel_target_filepath = os.path.basename(target_filepath)
50+
self.url = 'http://' + utils.TEST_HOST_ADDRESS + ':' \
51+
+ str(self.server_process_handler.port) + '/' + rel_target_filepath
52+
53+
# Create a temporary file where the target file chunks are written
54+
# during fetching
55+
self.temp_file = tempfile.TemporaryFile()
56+
self.fetcher = tuf.requests_fetcher.RequestsFetcher()
57+
58+
59+
# Stop server process and perform clean up.
60+
def tearDown(self):
61+
unittest_toolbox.Modified_TestCase.tearDown(self)
62+
63+
# Cleans the resources and flush the logged lines (if any).
64+
self.server_process_handler.clean()
65+
66+
self.target_fileobj.close()
67+
self.temp_file.close()
68+
69+
70+
# Test: Normal case.
71+
def test_fetch(self):
72+
for chunk in self.fetcher.fetch(self.url, self.file_length):
73+
self.temp_file.write(chunk)
74+
75+
self.temp_file.seek(0)
76+
temp_file_data = self.temp_file.read().decode('utf-8')
77+
self.assertEqual(self.file_contents, temp_file_data)
78+
79+
# Test if fetcher downloads file up to a required length
80+
def test_fetch_restricted_length(self):
81+
for chunk in self.fetcher.fetch(self.url, self.file_length-4):
82+
self.temp_file.write(chunk)
83+
84+
self.temp_file.seek(0, io.SEEK_END)
85+
self.assertEqual(self.temp_file.tell(), self.file_length-4)
86+
87+
88+
# Test that fetcher does not download more than actual file length
89+
def test_fetch_upper_length(self):
90+
for chunk in self.fetcher.fetch(self.url, self.file_length+4):
91+
self.temp_file.write(chunk)
92+
93+
self.temp_file.seek(0, io.SEEK_END)
94+
self.assertEqual(self.temp_file.tell(), self.file_length)
95+
96+
97+
# Test incorrect URL parsing
98+
def test_url_parsing(self):
99+
with self.assertRaises(tuf.exceptions.URLParsingError):
100+
self.fetcher.fetch(self.random_string(), self.file_length)
101+
102+
103+
# Test: Normal case with url data downloaded in more than one chunk
104+
def test_fetch_in_chunks(self):
105+
# Set smaller chunk size to ensure that the file will be downloaded
106+
# in more than one chunk
107+
default_chunk_size = tuf.settings.CHUNK_SIZE
108+
tuf.settings.CHUNK_SIZE = 4
109+
110+
# expected_chunks_count: 3
111+
expected_chunks_count = math.ceil(self.file_length/tuf.settings.CHUNK_SIZE)
112+
self.assertEqual(expected_chunks_count, 3)
113+
114+
chunks_count = 0
115+
for chunk in self.fetcher.fetch(self.url, self.file_length):
116+
self.temp_file.write(chunk)
117+
chunks_count+=1
118+
119+
self.temp_file.seek(0)
120+
temp_file_data = self.temp_file.read().decode('utf-8')
121+
self.assertEqual(self.file_contents, temp_file_data)
122+
# Check that we calculate chunks as expected
123+
self.assertEqual(chunks_count, expected_chunks_count)
124+
125+
# Restore default settings
126+
tuf.settings.CHUNK_SIZE = default_chunk_size
127+
128+
129+
130+
# Run unit test.
131+
if __name__ == '__main__':
132+
utils.configure_test_logging(sys.argv)
133+
unittest.main()

tests/test_indefinite_freeze_attack.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ def setUp(self):
146146
# Set the url prefix required by the 'tuf/client/updater.py' updater.
147147
# 'path/to/tmp/repository' -> 'localhost:8001/tmp/repository'.
148148
repository_basepath = self.repository_directory[len(os.getcwd()):]
149-
url_prefix = 'http://localhost:' \
149+
url_prefix = 'http://' + utils.TEST_HOST_ADDRESS + ':' \
150150
+ str(self.server_process_handler.port) + repository_basepath
151151

152152
# Setting 'tuf.settings.repository_directory' with the temporary client

tests/test_key_revocation_integration.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ def setUp(self):
132132

133133
# 'path/to/tmp/repository' -> 'localhost:8001/tmp/repository'.
134134
repository_basepath = self.repository_directory[len(os.getcwd()):]
135-
url_prefix = 'http://localhost:' \
135+
url_prefix = 'http://' + utils.TEST_HOST_ADDRESS + ':' \
136136
+ str(self.server_process_handler.port) + repository_basepath
137137

138138
# Setting 'tuf.settings.repository_directory' with the temporary client

0 commit comments

Comments
 (0)