python-tuf/tests/test_proxy_use.py

369 lines
12 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python
# Copyright 2018, New York University and the TUF contributors
# SPDX-License-Identifier: MIT OR Apache-2.0
"""
<Program>
test_proxy_use.py
<Copyright>
See LICENSE-MIT OR LICENSE for licensing information.
<Purpose>
Integration/regression test of TUF downloads through proxies.
NOTE: Make sure test_proxy_use.py is run in 'tuf/tests/' directory.
Otherwise, test data or scripts may not be found.
THIS module requires Python2.7 (not 2.8.x, not 3.x, just 2.7.x) as the test
proxy it uses only supports Python2.7.
So long as the tests succeed in Python 2.7, it is unlikely that TUF
behaves differently with respect to proxies when it runs in other Python
versions.
As a result of this dependency, this test is only run by aggregate_tests.py
when the Python version is 2.7.x.
"""
# Help with Python 3 compatibility, where the print statement is a function, an
# implicit relative import is invalid, and the '/' operator performs true
# division. Example: print 'hello world' raises a 'SyntaxError' exception.
from __future__ import print_function
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import logging
import os
import unittest
import sys
import tuf
import tuf.download as download
import tuf.log
import tuf.unittest_toolbox as unittest_toolbox
import tuf.exceptions
from tests import utils
import six
logger = logging.getLogger(__name__)
IS_PY_VERSION_SUPPORTED = sys.version_info == (2, 7)
# Use setUpModule to tell unittest runner to skip this test module gracefully.
def setUpModule():
if not IS_PY_VERSION_SUPPORTED:
raise unittest.SkipTest('requires Python 2.7')
class TestWithProxies(unittest_toolbox.Modified_TestCase):
@classmethod
def setUpClass(cls):
"""
Setup performed before the first test function (TestWithProxies class
method) runs.
Launch HTTP, HTTPS, and proxy servers in the current working directory.
We'll set up four servers:
- HTTP server (simple_server.py)
- HTTPS server (simple_https_server.py)
- HTTP proxy server (proxy_server.py)
(that supports HTTP CONNECT to funnel HTTPS connections)
- HTTPS proxy server (proxy_server.py)
(trusted by the client to intercept and resign connections)
"""
unittest_toolbox.Modified_TestCase.setUpClass()
if not six.PY2:
raise NotImplementedError("TestWithProxies only works with Python 2"
" (proxy_server.py is Python2 only)")
# Launch a simple HTTP server (serves files in the current dir).
cls.http_server_handler = utils.TestServerProcess(log=logger)
# Launch an HTTPS server (serves files in the current dir).
cls.https_server_handler = utils.TestServerProcess(log=logger,
Tests: Queue replace tmp files, OS port creation These changes can be summarized with the following bullets: - Delegate generation of ports used for the tests to the OS - Use thread-safe Queue for processes communication instead of temporary files - Remove all instances of port generation or hardcoded ports - Make test_slow_retrieval.py fully conform with TestServerProcess Delegate generation of ports used for the tests to the OS is much better than if we manually generate them, because there is always a chance that the port we have randomly pick turns out to be taken. By giving 0 to the port argument we ask the OS to give us an arbitrary unused port. Use thread-safe Queue for processes communication instead of temporary files became a necessity because of findings made by Jussi Kukkonen. With the latest changes made in pr 1192 we were rapidly reading from the temporary files and Jussi found that it happened rarely the successful message "bind succeded..." to be corrupted. It seems, this is a thread issue related to the thread redirecting the subprocess stdout to the temp file and our thread rapidly reading from the file. By using a thread-safe Queue we eliminate this possibility. For reference read: https://github.com/theupdateframework/tuf/issues/1196 Lastly, test_slow_retrieval.py and slow_retrieval.py were refactored. Until now, slow_retrieval.py couldn't use the TestServerProcess class from utils.py for a port generation because of a bug related to httpd.handle_request(). Now, when we use httpd.serve_forever() we can refactor both of those files and fully conform with TestServerProcess. Signed-off-by: Martin Vrachev <mvrachev@vmware.com>
2020-10-27 15:28:01 +00:00
server='simple_https_server.py')
# Launch an HTTP proxy server derived from inaz2/proxy2.
# This one is able to handle HTTP CONNECT requests, and so can pass HTTPS
# requests on to the target server.
cls.http_proxy_handler = utils.TestServerProcess(log=logger,
Tests: Queue replace tmp files, OS port creation These changes can be summarized with the following bullets: - Delegate generation of ports used for the tests to the OS - Use thread-safe Queue for processes communication instead of temporary files - Remove all instances of port generation or hardcoded ports - Make test_slow_retrieval.py fully conform with TestServerProcess Delegate generation of ports used for the tests to the OS is much better than if we manually generate them, because there is always a chance that the port we have randomly pick turns out to be taken. By giving 0 to the port argument we ask the OS to give us an arbitrary unused port. Use thread-safe Queue for processes communication instead of temporary files became a necessity because of findings made by Jussi Kukkonen. With the latest changes made in pr 1192 we were rapidly reading from the temporary files and Jussi found that it happened rarely the successful message "bind succeded..." to be corrupted. It seems, this is a thread issue related to the thread redirecting the subprocess stdout to the temp file and our thread rapidly reading from the file. By using a thread-safe Queue we eliminate this possibility. For reference read: https://github.com/theupdateframework/tuf/issues/1196 Lastly, test_slow_retrieval.py and slow_retrieval.py were refactored. Until now, slow_retrieval.py couldn't use the TestServerProcess class from utils.py for a port generation because of a bug related to httpd.handle_request(). Now, when we use httpd.serve_forever() we can refactor both of those files and fully conform with TestServerProcess. Signed-off-by: Martin Vrachev <mvrachev@vmware.com>
2020-10-27 15:28:01 +00:00
server='proxy_server.py')
# Note that the HTTP proxy server's address uses http://, regardless of the
# type of connection used with the target server.
cls.http_proxy_addr = 'http://127.0.0.1:' + str(cls.http_proxy_handler.port)
# Launch an HTTPS proxy server, also derived from inaz2/proxy2.
# (An HTTPS proxy performs its own TLS connection with the client and must
# be trusted by it, and is capable of tampering.)
# We instruct the proxy server to expect certain certificates from the
# target server.
# 1st arg: port
# 2nd arg: whether to intercept (HTTPS proxy) or relay (TCP tunnel using
# HTTP CONNECT verb, to facilitate an HTTPS connection between the client
# and server which the proxy cannot inspect)
# 3rd arg: (optional) certificate file for telling the proxy what target
# server certs to accept in its HTTPS connection to the target server.
# This is only relevant if the proxy is in intercept mode.
good_cert_fpath = os.path.join('ssl_certs', 'ssl_cert.crt')
cls.https_proxy_handler = utils.TestServerProcess(log=logger,
Tests: Queue replace tmp files, OS port creation These changes can be summarized with the following bullets: - Delegate generation of ports used for the tests to the OS - Use thread-safe Queue for processes communication instead of temporary files - Remove all instances of port generation or hardcoded ports - Make test_slow_retrieval.py fully conform with TestServerProcess Delegate generation of ports used for the tests to the OS is much better than if we manually generate them, because there is always a chance that the port we have randomly pick turns out to be taken. By giving 0 to the port argument we ask the OS to give us an arbitrary unused port. Use thread-safe Queue for processes communication instead of temporary files became a necessity because of findings made by Jussi Kukkonen. With the latest changes made in pr 1192 we were rapidly reading from the temporary files and Jussi found that it happened rarely the successful message "bind succeded..." to be corrupted. It seems, this is a thread issue related to the thread redirecting the subprocess stdout to the temp file and our thread rapidly reading from the file. By using a thread-safe Queue we eliminate this possibility. For reference read: https://github.com/theupdateframework/tuf/issues/1196 Lastly, test_slow_retrieval.py and slow_retrieval.py were refactored. Until now, slow_retrieval.py couldn't use the TestServerProcess class from utils.py for a port generation because of a bug related to httpd.handle_request(). Now, when we use httpd.serve_forever() we can refactor both of those files and fully conform with TestServerProcess. Signed-off-by: Martin Vrachev <mvrachev@vmware.com>
2020-10-27 15:28:01 +00:00
server='proxy_server.py', extra_cmd_args=['intercept',
good_cert_fpath])
# Note that the HTTPS proxy server's address uses https://, regardless of
# the type of connection used with the target server.
cls.https_proxy_addr = 'https://localhost:' + str(cls.https_proxy_handler.port)
@classmethod
def tearDownClass(cls):
"""
Cleanup performed after the last of the tests (TestWithProxies methods)
has been run.
Stop server process and perform clean up.
"""
unittest_toolbox.Modified_TestCase.tearDownClass()
for proc_handler in [
cls.http_server_handler,
cls.https_server_handler,
cls.http_proxy_handler,
cls.https_proxy_handler,
]:
# Kill the SimpleHTTPServer process.
proc_handler.clean()
def setUp(self):
"""
Setup performed before EACH test function (TestWithProxies class method)
runs.
"""
unittest_toolbox.Modified_TestCase.setUp(self)
# Dictionary for saving environment values to restore.
self.old_env_values = {}
# Make a temporary file to serve on the server, and determine its length,
# and its url on the server.
current_dir = os.getcwd()
target_filepath = self.make_temp_data_file(directory=current_dir)
with open(target_filepath, 'r') as target_file_object:
self.target_data_length = len(target_file_object.read())
suffix = '/' + os.path.basename(target_filepath)
self.url = \
'http://localhost:' + str(self.http_server_handler.port) + suffix
self.url_https = \
'https://localhost:' + str(self.https_server_handler.port) + suffix
def tearDown(self):
"""
Cleanup performed after each test (each TestWithProxies method).
Reset environment variables (for next test, etc.).
"""
unittest_toolbox.Modified_TestCase.tearDown(self)
self.restore_all_modified_env_values()
for proc_handler in [
self.http_server_handler,
self.https_server_handler,
self.http_proxy_handler,
self.https_proxy_handler,
]:
# Logs stdout and stderr from the sever subprocess.
proc_handler.flush_log()
def test_baseline_no_proxy(self):
"""
Test a length-validating TUF download of a file through a proxy. Use an
HTTP proxy, and perform an HTTP connection with the final server.
"""
logger.info('Trying HTTP download with no proxy: ' + self.url)
download.safe_download(self.url, self.target_data_length)
download.unsafe_download(self.url, self.target_data_length)
def test_http_dl_via_smart_http_proxy(self):
"""
Test a length-validating TUF download of a file through a proxy. Use an
HTTP proxy normally, and make an HTTP connection with the final server.
"""
self.set_env_value('HTTP_PROXY', self.http_proxy_addr)
logger.info('Trying HTTP download via HTTP proxy: ' + self.url)
download.safe_download(self.url, self.target_data_length)
download.unsafe_download(self.url, self.target_data_length)
def test_https_dl_via_smart_http_proxy(self):
"""
Test a length-validating TUF download of a file through a proxy. Use an
HTTP proxy that supports HTTP CONNECT (which essentially causes it to act
as a TCP proxy), and perform an HTTPS connection through with the final
server.
Note that the proxy address is still http://... even though the connection
with the target server is an HTTPS connection. The proxy itself will act as
a TCP proxy via HTTP CONNECT.
"""
self.set_env_value('HTTP_PROXY', self.http_proxy_addr) # http as intended
self.set_env_value('HTTPS_PROXY', self.http_proxy_addr) # http as intended
self.set_env_value('REQUESTS_CA_BUNDLE',
os.path.join('ssl_certs', 'ssl_cert.crt'))
# Clear sessions to ensure that the certificate we just specified is used.
# TODO: Confirm necessity of this session clearing and lay out mechanics.
tuf.download._sessions = {}
logger.info('Trying HTTPS download via HTTP proxy: ' + self.url_https)
download.safe_download(self.url_https, self.target_data_length)
download.unsafe_download(self.url_https, self.target_data_length)
def test_http_dl_via_https_proxy(self):
"""
Test a length-validating TUF download of a file through a proxy. Use an
HTTPS proxy, and perform an HTTP connection with the final server.
"""
self.set_env_value('HTTP_PROXY', self.https_proxy_addr)
self.set_env_value('HTTPS_PROXY', self.https_proxy_addr) # unnecessary
# We're making an HTTPS connection with the proxy. The proxy will make a
# plain HTTP connection to the target server.
self.set_env_value('REQUESTS_CA_BUNDLE',
os.path.join('ssl_certs', 'proxy_ca.crt'))
# Clear sessions to ensure that the certificate we just specified is used.
# TODO: Confirm necessity of this session clearing and lay out mechanics.
tuf.download._sessions = {}
logger.info('Trying HTTP download via HTTPS proxy: ' + self.url_https)
download.safe_download(self.url, self.target_data_length)
download.unsafe_download(self.url, self.target_data_length)
def test_https_dl_via_https_proxy(self):
"""
Test a length-validating TUF download of a file through a proxy. Use an
HTTPS proxy, and perform an HTTPS connection with the final server.
"""
self.set_env_value('HTTP_PROXY', self.https_proxy_addr) # unnecessary
self.set_env_value('HTTPS_PROXY', self.https_proxy_addr)
# We're making an HTTPS connection with the proxy. The proxy will make its
# own HTTPS connection with the target server, and will have to know what
# certificate to trust. It was told what certs to trust when it was
# started in setUpClass().
self.set_env_value('REQUESTS_CA_BUNDLE',
os.path.join('ssl_certs', 'proxy_ca.crt'))
# Clear sessions to ensure that the certificate we just specified is used.
# TODO: Confirm necessity of this session clearing and lay out mechanics.
tuf.download._sessions = {}
logger.info('Trying HTTPS download via HTTPS proxy: ' + self.url_https)
download.safe_download(self.url_https, self.target_data_length)
download.unsafe_download(self.url_https, self.target_data_length)
def set_env_value(self, key, value):
"""
Set an environment variable after noting what the original value was, if it
was set, and add it to the queue for restoring to its original value / lack
of a value after the test finishes.
Safe for multiple uses in one test: does not overwrite original saved value
with new saved values.
"""
# Only save the current value if we have not previously saved an older
# value. The original one is the one we'll restore to, not whatever we
# most recently overwrote.
if key not in self.old_env_values:
# If the value was previously unset in os.environ, save the old value
# as None so that we know to unset it.
self.old_env_values[key] = os.environ.get(key, None)
# Actually set the new value.
os.environ[key] = value
def restore_env_value(self, key):
# Save old values for environment variables for restoration after the test.
# Save the pre-existing value of the environment variables HTTP_PROXY and
# HTTPS_PROXY so that we can restore them in tearDown() after the test.
# If the value was not originally set at all, we'll try to unset it again,
# too.
assert key in self.old_env_values, 'Test coding mistake: something is ' \
'trying to restore environment variable ' + key + ', but that ' \
'variable does not appear in the list of values to restore. ' \
'Please make sure to use set_env_value().'
if self.old_env_values[key] is None:
# If it was not previously set, try to unset it.
# If the platform provides a way to unset environment variables,
# del os.environ[key] should unset the variable. Otherwise, we'll just
# have to settle for setting it to an empty string.
# See os.environ in:
# https://docs.python.org/2/library/os.html#process-parameters
os.environ[key] = ''
del os.environ[key]
else:
# If it was previously set, restore the original value from when the
# test was being set up.
os.environ[key] = self.old_env_values[key]
def restore_all_modified_env_values(self):
for key in self.old_env_values:
self.restore_env_value(key)
# Run unit test.
if __name__ == '__main__':
utils.configure_test_logging(sys.argv)
unittest.main()