mirror of
https://github.com/theupdateframework/python-tuf
synced 2026-05-24 10:08:28 +00:00
Merge branch 'trishankatdatadog/fix-for-https-proxies' into develop
Signed-off-by: Sebastien Awwad <sebastien.awwad@gmail.com>
This commit is contained in:
commit
9cd2d3a0ab
24 changed files with 1463 additions and 334 deletions
|
|
@ -4,3 +4,4 @@ iso8601
|
|||
coverage
|
||||
coveralls
|
||||
pylint
|
||||
requests
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ pylint==2.1.1 ; python_version >= "3.0"
|
|||
pylint==1.9.3 ; python_version < "3.0" # pyup: ignore
|
||||
pynacl==1.2.1
|
||||
pyyaml==3.13
|
||||
requests==2.19.1
|
||||
securesystemslib[crypto,pynacl]==0.11.2
|
||||
singledispatch==3.4.0.3
|
||||
six==1.11.0
|
||||
|
|
|
|||
|
|
@ -4,5 +4,6 @@ securesystemslib
|
|||
cryptography
|
||||
colorama
|
||||
pynacl
|
||||
requests
|
||||
six
|
||||
iso8601
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ asn1crypto==0.24.0 \
|
|||
--hash=sha256:2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87 \
|
||||
--hash=sha256:9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49 \
|
||||
# via cryptography
|
||||
certifi==2018.8.24 \
|
||||
--hash=sha256:376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638 \
|
||||
--hash=sha256:456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a \
|
||||
# via requests
|
||||
cffi==1.11.5 \
|
||||
--hash=sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743 \
|
||||
--hash=sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef \
|
||||
|
|
@ -37,6 +41,10 @@ cffi==1.11.5 \
|
|||
--hash=sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f \
|
||||
--hash=sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb \
|
||||
# via cryptography, pynacl
|
||||
chardet==3.0.4 \
|
||||
--hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \
|
||||
--hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691 \
|
||||
# via requests
|
||||
colorama==0.3.9 \
|
||||
--hash=sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda \
|
||||
--hash=sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1
|
||||
|
|
@ -69,11 +77,7 @@ enum34==1.1.6 \
|
|||
idna==2.7 \
|
||||
--hash=sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e \
|
||||
--hash=sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16 \
|
||||
# via cryptography
|
||||
ipaddress==1.0.22 \
|
||||
--hash=sha256:64b28eec5e78e7510698f6d4da08800a5c575caa4a286c93d651c5d3ff7b6794 \
|
||||
--hash=sha256:b146c751ea45cad6188dd6cf2d9b757f6f4f8d6ffb96a023e6f2e26eea02a72c \
|
||||
# via cryptography
|
||||
# via cryptography, requests
|
||||
iso8601==0.1.12 \
|
||||
--hash=sha256:210e0134677cc0d02f6028087fee1df1e1d76d372ee1db0bf30bf66c5c1c89a3 \
|
||||
--hash=sha256:49c4b20e1f38aa5cf109ddcd39647ac419f928512c869dc01d5c7098eddede82 \
|
||||
|
|
@ -105,9 +109,16 @@ pynacl==1.2.1 \
|
|||
--hash=sha256:eeee629828d0eb4f6d98ac41e9a3a6461d114d1d0aa111a8931c049359298da0 \
|
||||
--hash=sha256:f5ce9e26d25eb0b2d96f3ef0ad70e1d3ae89b5d60255c462252a3e456a48c053 \
|
||||
--hash=sha256:fabf73d5d0286f9e078774f3435601d2735c94ce9e514ac4fb945701edead7e4
|
||||
requests==2.19.1 \
|
||||
--hash=sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1 \
|
||||
--hash=sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a
|
||||
securesystemslib==0.11.2 \
|
||||
--hash=sha256:43554371feeef50196587aa066cffd6b9ceff6b484fa7b127e139fafb5c0e23e \
|
||||
--hash=sha256:7fe1ed8a4139b12225986ff6f9ebab48c74eaa93265a73f988e8de10e6b237a8
|
||||
six==1.11.0 \
|
||||
--hash=sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9 \
|
||||
--hash=sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb
|
||||
urllib3==1.23 \
|
||||
--hash=sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf \
|
||||
--hash=sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5 \
|
||||
# via requests
|
||||
|
|
|
|||
10
setup.py
10
setup.py
|
|
@ -78,9 +78,10 @@
|
|||
with open('README.md') as file_object:
|
||||
long_description = file_object.read()
|
||||
|
||||
|
||||
setup(
|
||||
name = 'tuf',
|
||||
version = '0.11.1',
|
||||
version = '0.11.1', # If updating version, also update it in tuf/__init__.py
|
||||
description = 'A secure updater framework for Python',
|
||||
long_description = long_description,
|
||||
long_description_content_type='text/markdown',
|
||||
|
|
@ -109,7 +110,12 @@
|
|||
'Topic :: Security',
|
||||
'Topic :: Software Development'
|
||||
],
|
||||
install_requires = ['iso8601>=0.1.12', 'six>=1.11.0', 'securesystemslib>=0.11.2'],
|
||||
install_requires = [
|
||||
'iso8601>=0.1.12',
|
||||
'requests>=2.19.1',
|
||||
'six>=1.11.0',
|
||||
'securesystemslib>=0.11.2'
|
||||
],
|
||||
packages = find_packages(exclude=['tests']),
|
||||
scripts = [
|
||||
'tuf/scripts/repo.py',
|
||||
|
|
|
|||
|
|
@ -43,22 +43,54 @@
|
|||
# 'test_' and end with '.py'. A shell-style wildcard is used with glob() to
|
||||
# match desired filenames. All the tests matching the pattern will be loaded
|
||||
# and run in a test suite.
|
||||
tests_list = glob.glob('test_*.py')
|
||||
available_tests = glob.glob('test_*.py')
|
||||
|
||||
# Remove '.py' from each filename to allow loadTestsFromNames() (called below)
|
||||
# to properly load the file as a module.
|
||||
tests_without_extension = []
|
||||
for test in tests_list:
|
||||
# A dictionary of test modules that should only run in certain python versions.
|
||||
# Carefully consider the impact of only testing these in a given version.
|
||||
# test_proxy_use.py: uses a proxy that only runs in Python2.7. TUF's
|
||||
# compatibility with proxies is not likely to vary based on the Python version
|
||||
# in use, so this is OK for now. See comments in that module.
|
||||
# The semantics here are: only add to this list the particular tests that are
|
||||
# to be run in a single major version or a single minor version. An entry must
|
||||
# include major version, and may include minor version.
|
||||
# Skip the test if any such listed constraints don't match the python version
|
||||
# currently running.
|
||||
# Note that aggregate_tests.py is run for each version of Python that tox is
|
||||
# configured to use. Note also that this TUF implementation does not support
|
||||
# any Python versions <2.7 or any Python3 versions <3.4.
|
||||
VERSION_SPECIFIC_TESTS = {
|
||||
'test_proxy_use': {'major': 2, 'minor': 7}} # Run test only if Python2.7
|
||||
# Further example:
|
||||
# 'test_abc': {'major': 2} # Run test only if Python2
|
||||
|
||||
# Determine which tests should be run.
|
||||
test_modules_to_run = []
|
||||
for test in available_tests:
|
||||
# Remove '.py' from each filename to allow loadTestsFromNames() (called below)
|
||||
# to properly load the file as a module.
|
||||
assert test[-3:] == '.py', 'aggregate_tests.py is inconsistent; fix.'
|
||||
test = test[:-3]
|
||||
tests_without_extension.append(test)
|
||||
|
||||
if test in VERSION_SPECIFIC_TESTS:
|
||||
# Consistency checks.
|
||||
assert 'major' in VERSION_SPECIFIC_TESTS[test], 'Empty/illogical constraint'
|
||||
for keyword in VERSION_SPECIFIC_TESTS[test]:
|
||||
assert keyword in ['major', 'minor'], 'Unrecognized test constraint'
|
||||
|
||||
if sys.version_info.major != VERSION_SPECIFIC_TESTS[test]['major']:
|
||||
continue
|
||||
if 'minor' in VERSION_SPECIFIC_TESTS[test] \
|
||||
and sys.version_info.minor != VERSION_SPECIFIC_TESTS[test]['minor']:
|
||||
continue
|
||||
test_modules_to_run.append(test)
|
||||
|
||||
# Randomize the order in which the tests run. Randomization might catch errors
|
||||
# with unit tests that do not properly clean up or restore monkey-patched
|
||||
# modules.
|
||||
random.shuffle(tests_without_extension)
|
||||
random.shuffle(test_modules_to_run)
|
||||
|
||||
if __name__ == '__main__':
|
||||
suite = unittest.TestLoader().loadTestsFromNames(tests_without_extension)
|
||||
suite = unittest.TestLoader().loadTestsFromNames(test_modules_to_run)
|
||||
all_tests_passed = unittest.TextTestRunner(verbosity=1).run(suite).wasSuccessful()
|
||||
if not all_tests_passed:
|
||||
sys.exit(1)
|
||||
|
|
|
|||
500
tests/proxy_server.py
Normal file
500
tests/proxy_server.py
Normal file
|
|
@ -0,0 +1,500 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# This code is taken from: github.com/inaz2/proxy2
|
||||
# Credit goes to the author. It has been very slightly modified here to use
|
||||
# IPv4 instead of IPv6, and to only attempt interception of HTTPS traffic
|
||||
# (instead of relaying via HTTP CONNECT) if new global variable INTERCEPT is
|
||||
# set to True. (Modified sections are marked '# MODIFIED'.)
|
||||
#
|
||||
# Because this is a helper module for a test, the style is less important, and
|
||||
# so to minimize changes from the source, it has NOT been changed to match the
|
||||
# TUF project's code style outside of rewritten sections.
|
||||
|
||||
"""
|
||||
<Program>
|
||||
proxy_server.py
|
||||
|
||||
<Copyright>
|
||||
Taken from a repository set to BSD 3-Clause "New" or "Revised" License. See:
|
||||
https://github.com/inaz2/proxy2/blob/b2bab648173ac69f0a10421750125517accdfe26/LICENSE
|
||||
|
||||
<Purpose>
|
||||
Serves as an HTTP, HTTP CONNECT (TCP), and HTTPS proxy, for testing purposes.
|
||||
This is used by test_proxy_use.py.
|
||||
|
||||
In Python versions < 2.7.9, this proxy does not perform certificate
|
||||
validation of the target server. As that is not part of what the current
|
||||
tests using this script require, that is currently OK. In Python
|
||||
versions > 2.7.9 (SSLContext was added in 2.7.9), the same code actually does
|
||||
check the certificate, using the system's trusted CAs. As a result, since we
|
||||
are using custom certificates, we need to either disable certificate
|
||||
checking in 2.7.9 or load the specific CA for target test server, using the
|
||||
SSLContext and create_default_context functionality also added in 2.7.9. It
|
||||
is easier to do the latter, so the behavior in 2.7.9+ is to check the cert
|
||||
and below 2.7.9 is not to. Note that we do not support Python < 2.7.
|
||||
SSLContext is also available in all Python3 versions that we support.
|
||||
|
||||
This module requires Python2.7 and does not support Python3.
|
||||
|
||||
Note that this is not thread-safe, in part due to its use of globals.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import socket
|
||||
import ssl
|
||||
import select
|
||||
import httplib
|
||||
import urlparse
|
||||
import threading
|
||||
import gzip
|
||||
import zlib
|
||||
import time
|
||||
import json
|
||||
import re
|
||||
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
|
||||
from SocketServer import ThreadingMixIn
|
||||
from cStringIO import StringIO
|
||||
from subprocess import Popen, PIPE
|
||||
from HTMLParser import HTMLParser
|
||||
|
||||
# MODIFIED: (added) three globals
|
||||
# INTERCEPT: A boolean:
|
||||
# False: normal HTTP proxy. Support HTTP & HTTPS connections to target server
|
||||
# True: intercepting MITM transparent HTTPS proxy. Makes own TLS connections
|
||||
# and has its own cert; must be trusted by the client and is able to
|
||||
# modify requests.
|
||||
# TARGET_SERVER_CA_FILEPATH: location of certificate to use as CA for
|
||||
# connections to target servers (to constrain certs to trust from target
|
||||
# servers).
|
||||
# The remaining globals define the certs and keys to be used in communications
|
||||
# with the client, with the proxy's CA signing new certs for individual hosts
|
||||
# the client wishes to connect to, and placing them in dir PROXY_CERTS_DIR.
|
||||
INTERCEPT = False
|
||||
CERTS_DIR = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), 'ssl_certs')
|
||||
TARGET_SERVER_CA_FILEPATH = os.path.join(CERTS_DIR, 'ssl_cert.crt')
|
||||
PROXY_CA_KEY = os.path.join(CERTS_DIR, 'proxy_ca.key') # was cakey
|
||||
PROXY_CA_CERT = os.path.join(CERTS_DIR, 'proxy_ca.crt') # was cacert
|
||||
PROXY_CERTS_KEY = os.path.join(CERTS_DIR, 'proxy_cert.key') # was certkey
|
||||
PROXY_CERTS_DIR = os.path.join(CERTS_DIR, 'proxy_certs') # was certdir
|
||||
|
||||
|
||||
def with_color(c, s):
|
||||
return "\x1b[%dm%s\x1b[0m" % (c, s)
|
||||
|
||||
# MODIFIED: removed join_with_script_dir
|
||||
# def get_cert_filepath(path):
|
||||
# return os.path.join(CERTS_DIR, path)
|
||||
|
||||
|
||||
class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
|
||||
address_family = socket.AF_INET # MODIFIED to use IPv4 instead of IPv6
|
||||
daemon_threads = True
|
||||
|
||||
def handle_error(self, request, client_address):
|
||||
# surpress socket/ssl related errors
|
||||
cls, e = sys.exc_info()[:2]
|
||||
if cls is socket.error or cls is ssl.SSLError:
|
||||
pass
|
||||
else:
|
||||
return HTTPServer.handle_error(self, request, client_address)
|
||||
|
||||
|
||||
class ProxyRequestHandler(BaseHTTPRequestHandler):
|
||||
# MODIFIED: Variables here made into globals.
|
||||
#Calls below modified: filenames changed, function changed to
|
||||
# include ssl_certs directory.
|
||||
timeout = 5
|
||||
lock = threading.Lock()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.tls = threading.local()
|
||||
self.tls.conns = {}
|
||||
|
||||
BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
|
||||
|
||||
def log_error(self, format, *args):
|
||||
# surpress "Request timed out: timeout('timed out',)"
|
||||
if isinstance(args[0], socket.timeout):
|
||||
return
|
||||
|
||||
self.log_message(format, *args)
|
||||
|
||||
def do_CONNECT(self):
|
||||
# MODIFIED: This function has been modified to use new global INTERCEPT
|
||||
# and to issue an error if the necessary certificate/key files are
|
||||
# missing for interception attempts.
|
||||
if not INTERCEPT:
|
||||
print('\n\nRELAYING\n\n')
|
||||
self.connect_relay()
|
||||
|
||||
else:
|
||||
assert os.path.isfile(PROXY_CA_KEY) \
|
||||
and os.path.isfile(PROXY_CA_CERT) \
|
||||
and os.path.isfile(PROXY_CERTS_KEY) \
|
||||
and os.path.isdir(PROXY_CERTS_DIR), \
|
||||
'\nMissing key or certificate files; unable to perform TLS ' \
|
||||
'handshake with client to intercept traffic.\n'
|
||||
print('\n\nINTERCEPTING\n\n')
|
||||
self.connect_intercept()
|
||||
|
||||
def connect_intercept(self):
|
||||
hostname = self.path.split(':')[0]
|
||||
certpath = os.path.join(PROXY_CERTS_DIR, hostname + '.crt') # MODIFIED for Windows compatibility and to use new globals
|
||||
|
||||
with self.lock:
|
||||
if not os.path.isfile(certpath):
|
||||
epoch = "%d" % (time.time() * 1000)
|
||||
p1 = Popen(["openssl", "req", "-new", "-key", PROXY_CERTS_KEY, "-subj", "/CN=%s" % hostname], stdout=PIPE)
|
||||
p2 = Popen(["openssl", "x509", "-req", "-days", "3650", "-CA", PROXY_CA_CERT, "-CAkey", PROXY_CA_KEY, "-set_serial", epoch, "-out", certpath], stdin=p1.stdout, stderr=PIPE) # MODIFIED to use the new globals
|
||||
p2.communicate()
|
||||
|
||||
self.wfile.write("%s %d %s\r\n" % (self.protocol_version, 200, 'Connection Established'))
|
||||
self.end_headers()
|
||||
|
||||
self.connection = ssl.wrap_socket(self.connection, keyfile=PROXY_CERTS_KEY, certfile=certpath, server_side=True) # MODIFIED: Updated to use new globals
|
||||
self.rfile = self.connection.makefile("rb", self.rbufsize)
|
||||
self.wfile = self.connection.makefile("wb", self.wbufsize)
|
||||
|
||||
conntype = self.headers.get('Proxy-Connection', '')
|
||||
if self.protocol_version == "HTTP/1.1" and conntype.lower() != 'close':
|
||||
self.close_connection = 0
|
||||
else:
|
||||
self.close_connection = 1
|
||||
|
||||
def connect_relay(self):
|
||||
address = self.path.split(':', 1)
|
||||
address[1] = int(address[1]) or 443
|
||||
try:
|
||||
s = socket.create_connection(address, timeout=self.timeout)
|
||||
except Exception as e:
|
||||
self.send_error(502)
|
||||
return
|
||||
self.send_response(200, 'Connection Established')
|
||||
self.end_headers()
|
||||
|
||||
conns = [self.connection, s]
|
||||
self.close_connection = 0
|
||||
while not self.close_connection:
|
||||
rlist, wlist, xlist = select.select(conns, [], conns, self.timeout)
|
||||
if xlist or not rlist:
|
||||
break
|
||||
for r in rlist:
|
||||
other = conns[1] if r is conns[0] else conns[0]
|
||||
data = r.recv(8192)
|
||||
if not data:
|
||||
self.close_connection = 1
|
||||
break
|
||||
other.sendall(data)
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == 'http://proxy2.test/':
|
||||
self.send_cacert()
|
||||
return
|
||||
|
||||
req = self
|
||||
content_length = int(req.headers.get('Content-Length', 0))
|
||||
req_body = self.rfile.read(content_length) if content_length else None
|
||||
|
||||
if req.path[0] == '/':
|
||||
if isinstance(self.connection, ssl.SSLSocket):
|
||||
req.path = "https://%s%s" % (req.headers['Host'], req.path)
|
||||
else:
|
||||
req.path = "http://%s%s" % (req.headers['Host'], req.path)
|
||||
|
||||
req_body_modified = self.request_handler(req, req_body)
|
||||
if req_body_modified is False:
|
||||
self.send_error(403)
|
||||
return
|
||||
elif req_body_modified is not None:
|
||||
req_body = req_body_modified
|
||||
req.headers['Content-length'] = str(len(req_body))
|
||||
|
||||
u = urlparse.urlsplit(req.path)
|
||||
scheme, netloc, path = u.scheme, u.netloc, (u.path + '?' + u.query if u.query else u.path)
|
||||
assert scheme in ('http', 'https')
|
||||
if netloc:
|
||||
req.headers['Host'] = netloc
|
||||
setattr(req, 'headers', self.filter_headers(req.headers))
|
||||
|
||||
try:
|
||||
origin = (scheme, netloc)
|
||||
if not origin in self.tls.conns:
|
||||
if scheme == 'https':
|
||||
# MODIFIED: Added Python version checking and changed behavior
|
||||
# in Python2.7.9+ to use custom certificate for target server
|
||||
# inherited from command line argument.
|
||||
# In Python versions < 2.7.9, there is no certificate
|
||||
# validation through this method of the target server.
|
||||
# In supported Python versions > 2.7.9, we check the target
|
||||
# server's certificate against our expected custom cert.
|
||||
# See this script's docstring.
|
||||
if sys.version_info.major == 2 \
|
||||
and sys.version_info.minor == 7 \
|
||||
and sys.version_info.micro < 9:
|
||||
self.tls.conns[origin] = httplib.HTTPSConnection(
|
||||
netloc, timeout=self.timeout)
|
||||
else:
|
||||
self.tls.conns[origin] = httplib.HTTPSConnection(
|
||||
netloc, timeout=self.timeout,
|
||||
context=ssl.create_default_context( # reqs Python2.7.9+
|
||||
cafile=TARGET_SERVER_CA_FILEPATH))
|
||||
else:
|
||||
self.tls.conns[origin] = httplib.HTTPConnection(netloc, timeout=self.timeout)
|
||||
conn = self.tls.conns[origin]
|
||||
conn.request(self.command, path, req_body, dict(req.headers))
|
||||
res = conn.getresponse()
|
||||
|
||||
version_table = {10: 'HTTP/1.0', 11: 'HTTP/1.1'}
|
||||
setattr(res, 'headers', res.msg)
|
||||
setattr(res, 'response_version', version_table[res.version])
|
||||
|
||||
# support streaming
|
||||
if not 'Content-Length' in res.headers and 'no-store' in res.headers.get('Cache-Control', ''):
|
||||
self.response_handler(req, req_body, res, '')
|
||||
setattr(res, 'headers', self.filter_headers(res.headers))
|
||||
self.relay_streaming(res)
|
||||
with self.lock:
|
||||
self.save_handler(req, req_body, res, '')
|
||||
return
|
||||
|
||||
res_body = res.read()
|
||||
except Exception as e:
|
||||
if origin in self.tls.conns:
|
||||
del self.tls.conns[origin]
|
||||
self.send_error(502)
|
||||
return
|
||||
|
||||
content_encoding = res.headers.get('Content-Encoding', 'identity')
|
||||
res_body_plain = self.decode_content_body(res_body, content_encoding)
|
||||
|
||||
res_body_modified = self.response_handler(req, req_body, res, res_body_plain)
|
||||
if res_body_modified is False:
|
||||
self.send_error(403)
|
||||
return
|
||||
elif res_body_modified is not None:
|
||||
res_body_plain = res_body_modified
|
||||
res_body = self.encode_content_body(res_body_plain, content_encoding)
|
||||
res.headers['Content-Length'] = str(len(res_body))
|
||||
|
||||
setattr(res, 'headers', self.filter_headers(res.headers))
|
||||
|
||||
self.wfile.write("%s %d %s\r\n" % (self.protocol_version, res.status, res.reason))
|
||||
for line in res.headers.headers:
|
||||
self.wfile.write(line)
|
||||
self.end_headers()
|
||||
self.wfile.write(res_body)
|
||||
self.wfile.flush()
|
||||
|
||||
with self.lock:
|
||||
self.save_handler(req, req_body, res, res_body_plain)
|
||||
|
||||
def relay_streaming(self, res):
|
||||
self.wfile.write("%s %d %s\r\n" % (self.protocol_version, res.status, res.reason))
|
||||
for line in res.headers.headers:
|
||||
self.wfile.write(line)
|
||||
self.end_headers()
|
||||
try:
|
||||
while True:
|
||||
chunk = res.read(8192)
|
||||
if not chunk:
|
||||
break
|
||||
self.wfile.write(chunk)
|
||||
self.wfile.flush()
|
||||
except socket.error:
|
||||
# connection closed by client
|
||||
pass
|
||||
|
||||
do_HEAD = do_GET
|
||||
do_POST = do_GET
|
||||
do_PUT = do_GET
|
||||
do_DELETE = do_GET
|
||||
do_OPTIONS = do_GET
|
||||
|
||||
def filter_headers(self, headers):
|
||||
# http://tools.ietf.org/html/rfc2616#section-13.5.1
|
||||
hop_by_hop = ('connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailers', 'transfer-encoding', 'upgrade')
|
||||
for k in hop_by_hop:
|
||||
del headers[k]
|
||||
|
||||
# accept only supported encodings
|
||||
if 'Accept-Encoding' in headers:
|
||||
ae = headers['Accept-Encoding']
|
||||
filtered_encodings = [x for x in re.split(r',\s*', ae) if x in ('identity', 'gzip', 'x-gzip', 'deflate')]
|
||||
headers['Accept-Encoding'] = ', '.join(filtered_encodings)
|
||||
|
||||
return headers
|
||||
|
||||
def encode_content_body(self, text, encoding):
|
||||
if encoding == 'identity':
|
||||
data = text
|
||||
elif encoding in ('gzip', 'x-gzip'):
|
||||
io = StringIO()
|
||||
with gzip.GzipFile(fileobj=io, mode='wb') as f:
|
||||
f.write(text)
|
||||
data = io.getvalue()
|
||||
elif encoding == 'deflate':
|
||||
data = zlib.compress(text)
|
||||
else:
|
||||
raise Exception("Unknown Content-Encoding: %s" % encoding)
|
||||
return data
|
||||
|
||||
def decode_content_body(self, data, encoding):
|
||||
if encoding == 'identity':
|
||||
text = data
|
||||
elif encoding in ('gzip', 'x-gzip'):
|
||||
io = StringIO(data)
|
||||
with gzip.GzipFile(fileobj=io) as f:
|
||||
text = f.read()
|
||||
elif encoding == 'deflate':
|
||||
try:
|
||||
text = zlib.decompress(data)
|
||||
except zlib.error:
|
||||
text = zlib.decompress(data, -zlib.MAX_WBITS)
|
||||
else:
|
||||
raise Exception("Unknown Content-Encoding: %s" % encoding)
|
||||
return text
|
||||
|
||||
def send_cacert(self):
|
||||
with open(PROXY_CA_CERT, 'rb') as f: # MODIFIED to use new globals
|
||||
data = f.read()
|
||||
|
||||
self.wfile.write("%s %d %s\r\n" % (self.protocol_version, 200, 'OK'))
|
||||
self.send_header('Content-Type', 'application/x-x509-ca-cert')
|
||||
self.send_header('Content-Length', len(data))
|
||||
self.send_header('Connection', 'close')
|
||||
self.end_headers()
|
||||
self.wfile.write(data)
|
||||
|
||||
def print_info(self, req, req_body, res, res_body):
|
||||
def parse_qsl(s):
|
||||
return '\n'.join("%-20s %s" % (k, v) for k, v in urlparse.parse_qsl(s, keep_blank_values=True))
|
||||
|
||||
req_header_text = "%s %s %s\n%s" % (req.command, req.path, req.request_version, req.headers)
|
||||
res_header_text = "%s %d %s\n%s" % (res.response_version, res.status, res.reason, res.headers)
|
||||
|
||||
print with_color(33, req_header_text)
|
||||
|
||||
u = urlparse.urlsplit(req.path)
|
||||
if u.query:
|
||||
query_text = parse_qsl(u.query)
|
||||
print with_color(32, "==== QUERY PARAMETERS ====\n%s\n" % query_text)
|
||||
|
||||
cookie = req.headers.get('Cookie', '')
|
||||
if cookie:
|
||||
cookie = parse_qsl(re.sub(r';\s*', '&', cookie))
|
||||
print with_color(32, "==== COOKIE ====\n%s\n" % cookie)
|
||||
|
||||
auth = req.headers.get('Authorization', '')
|
||||
if auth.lower().startswith('basic'):
|
||||
token = auth.split()[1].decode('base64')
|
||||
print with_color(31, "==== BASIC AUTH ====\n%s\n" % token)
|
||||
|
||||
if req_body is not None:
|
||||
req_body_text = None
|
||||
content_type = req.headers.get('Content-Type', '')
|
||||
|
||||
if content_type.startswith('application/x-www-form-urlencoded'):
|
||||
req_body_text = parse_qsl(req_body)
|
||||
elif content_type.startswith('application/json'):
|
||||
try:
|
||||
json_obj = json.loads(req_body)
|
||||
json_str = json.dumps(json_obj, indent=2)
|
||||
if json_str.count('\n') < 50:
|
||||
req_body_text = json_str
|
||||
else:
|
||||
lines = json_str.splitlines()
|
||||
req_body_text = "%s\n(%d lines)" % ('\n'.join(lines[:50]), len(lines))
|
||||
except ValueError:
|
||||
req_body_text = req_body
|
||||
elif len(req_body) < 1024:
|
||||
req_body_text = req_body
|
||||
|
||||
if req_body_text:
|
||||
print with_color(32, "==== REQUEST BODY ====\n%s\n" % req_body_text)
|
||||
|
||||
print with_color(36, res_header_text)
|
||||
|
||||
cookies = res.headers.getheaders('Set-Cookie')
|
||||
if cookies:
|
||||
cookies = '\n'.join(cookies)
|
||||
print with_color(31, "==== SET-COOKIE ====\n%s\n" % cookies)
|
||||
|
||||
if res_body is not None:
|
||||
res_body_text = None
|
||||
content_type = res.headers.get('Content-Type', '')
|
||||
|
||||
if content_type.startswith('application/json'):
|
||||
try:
|
||||
json_obj = json.loads(res_body)
|
||||
json_str = json.dumps(json_obj, indent=2)
|
||||
if json_str.count('\n') < 50:
|
||||
res_body_text = json_str
|
||||
else:
|
||||
lines = json_str.splitlines()
|
||||
res_body_text = "%s\n(%d lines)" % ('\n'.join(lines[:50]), len(lines))
|
||||
except ValueError:
|
||||
res_body_text = res_body
|
||||
elif content_type.startswith('text/html'):
|
||||
m = re.search(r'<title[^>]*>\s*([^<]+?)\s*</title>', res_body, re.I)
|
||||
if m:
|
||||
h = HTMLParser()
|
||||
print with_color(32, "==== HTML TITLE ====\n%s\n" % h.unescape(m.group(1).decode('utf-8')))
|
||||
elif content_type.startswith('text/') and len(res_body) < 1024:
|
||||
res_body_text = res_body
|
||||
|
||||
if res_body_text:
|
||||
print with_color(32, "==== RESPONSE BODY ====\n%s\n" % res_body_text)
|
||||
|
||||
def request_handler(self, req, req_body):
|
||||
pass
|
||||
|
||||
def response_handler(self, req, req_body, res, res_body):
|
||||
pass
|
||||
|
||||
def save_handler(self, req, req_body, res, res_body):
|
||||
self.print_info(req, req_body, res, res_body)
|
||||
|
||||
|
||||
def test(HandlerClass=ProxyRequestHandler, ServerClass=ThreadingHTTPServer, protocol="HTTP/1.1"):
|
||||
# MODIFIED: Added these globals.
|
||||
global INTERCEPT
|
||||
global TARGET_SERVER_CA_FILEPATH
|
||||
|
||||
if sys.argv[1:]:
|
||||
port = int(sys.argv[1])
|
||||
else:
|
||||
port = 8080
|
||||
server_address = ('127.0.0.1', port) # MODIFIED: changed from '::1'
|
||||
|
||||
# MODIFIED: Argument added, conditional below added to control INTERCEPT
|
||||
# setting.
|
||||
if len(sys.argv) > 2:
|
||||
if sys.argv[2].lower() == 'intercept':
|
||||
INTERCEPT = True
|
||||
|
||||
# MODIFIED: Argument added to control certificate(s) the proxy expects of
|
||||
# the target server(s), and added default value.
|
||||
if len(sys.argv) > 3:
|
||||
if os.path.exists(sys.argv[3]):
|
||||
TARGET_SERVER_CA_FILEPATH = sys.argv[3]
|
||||
else:
|
||||
raise Exception('Target server cert file not found: ' + sys.argv[3])
|
||||
|
||||
# MODIFIED: Create the target-host-specific proxy certificates directory if
|
||||
# it doesn't already exist.
|
||||
if not os.path.exists(PROXY_CERTS_DIR):
|
||||
os.mkdir(PROXY_CERTS_DIR)
|
||||
|
||||
|
||||
HandlerClass.protocol_version = protocol
|
||||
httpd = ServerClass(server_address, HandlerClass)
|
||||
|
||||
sa = httpd.socket.getsockname()
|
||||
print "Serving HTTP Proxy on", sa[0], "port", sa[1], "..."
|
||||
httpd.serve_forever()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
test()
|
||||
|
|
@ -40,11 +40,14 @@
|
|||
import sys
|
||||
import random
|
||||
import ssl
|
||||
|
||||
import os
|
||||
import six
|
||||
|
||||
PORT = 0
|
||||
|
||||
keyfile = os.path.join('ssl_certs', 'ssl_cert.key')
|
||||
certfile = os.path.join('ssl_certs', 'ssl_cert.crt')
|
||||
|
||||
def _generate_random_port():
|
||||
return random.randint(30000, 45000)
|
||||
|
||||
|
|
@ -60,12 +63,19 @@ def _generate_random_port():
|
|||
else:
|
||||
PORT = _generate_random_port()
|
||||
|
||||
if len(sys.argv) > 2:
|
||||
|
||||
if os.path.exists(sys.argv[2]):
|
||||
certfile = sys.argv[2]
|
||||
else:
|
||||
print('simple_https_server: cert file not found: ' + sys.argv[2] +
|
||||
'; using default: ' + certfile)
|
||||
|
||||
httpd = six.moves.BaseHTTPServer.HTTPServer(('localhost', PORT),
|
||||
six.moves.SimpleHTTPServer.SimpleHTTPRequestHandler)
|
||||
|
||||
httpd.socket = ssl.wrap_socket(httpd.socket, keyfile='ssl_cert.key',
|
||||
certfile='ssl_cert.crt',
|
||||
server_side=True)
|
||||
httpd.socket = ssl.wrap_socket(
|
||||
httpd.socket, keyfile=keyfile, certfile=certfile, server_side=True)
|
||||
|
||||
#print('Starting https server on port: ' + str(PORT))
|
||||
httpd.serve_forever()
|
||||
|
|
|
|||
|
|
@ -73,11 +73,7 @@ def do_GET(self):
|
|||
# 'mode_2'
|
||||
else:
|
||||
DELAY = 1
|
||||
# Throttle the file by sending a character every few seconds.
|
||||
# NOTE: The for-loop below completes early if the download file
|
||||
# (len(data)) is small. 'download.py' waits at least
|
||||
# 'settings.SLOW_START_GRACE_PERIOD' seconds before triggering a
|
||||
# potential slow retrieval error.
|
||||
# Throttle the file by sending a character every DELAY seconds.
|
||||
for i in range(len(data)):
|
||||
self.wfile.write(data[i].encode('utf-8'))
|
||||
time.sleep(DELAY)
|
||||
|
|
|
|||
17
tests/ssl_certs/proxy_ca.crt
Normal file
17
tests/ssl_certs/proxy_ca.crt
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIICpDCCAYwCCQCFr/EhHmzVajANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAlw
|
||||
cm94eTIgQ0EwHhcNMTgwOTIwMTkyOTQ2WhcNMjgwOTE3MTkyOTQ2WjAUMRIwEAYD
|
||||
VQQDDAlwcm94eTIgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC/
|
||||
rVOeqSzJb01Vyliw3dnfLJsWfDfs/Lq5HLn+Xqnzl6MqnYirDqHzTErD3vl8lo/o
|
||||
OJrziO0vYCWGXEylRQlZp+P37bLToSWiVqWZ8pH6CAh+AhA3WtegN5JwTgIUSP7A
|
||||
aDlxuZrXlJM50QVlXJIPkc74M8ALz0nu5zmyWkGFvmTYS8503T8cXs9Alr4Bo++9
|
||||
Ilixv6lW4QS7FKTeQXlI49K4TeGGGsfmEO6Uj4WTUkwMZym9wfiqtaWc6I9ZMese
|
||||
WmU3LuufY+pFCdjsdMWDJpYc+HabTSrbgXSF5Iq9a84Xuum39qhVpYhBwBtLk3ye
|
||||
cxZmIxde1vnkWAitJFETAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAKV09r/x3WyO
|
||||
McH0RU4WRVzvQN5F0e7swpDlLUX7YnfvpPEkavqQfmrL1cYyEDgsm/347Gvcs1Aa
|
||||
iaT77axYroXOvCEJ3DxZdzUErKH6Jr3MmHKcZ/L35u6ZXKnmx/edFjdWr6ENkjuZ
|
||||
NVvKbTrm4cl6Wy4bXkp6b24rBa9IFJncOouSkIvHENEcH//OD4xeTK8vSJTJ9nmw
|
||||
TiJ0TjCRujtJWC6yb03ZV32VbeiHa1zLlZhcyKqUtt81dLti5t5+L2hAAVCcnEgI
|
||||
DBWQdlRs/wilHGWVBo/9srOoMNsmvecTBpLH2JyC5VZ1+faYLPrNlgkWgHIFOTTi
|
||||
h4ByR95Wbi8=
|
||||
-----END CERTIFICATE-----
|
||||
27
tests/ssl_certs/proxy_ca.key
Normal file
27
tests/ssl_certs/proxy_ca.key
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpQIBAAKCAQEAv61TnqksyW9NVcpYsN3Z3yybFnw37Py6uRy5/l6p85ejKp2I
|
||||
qw6h80xKw975fJaP6Dia84jtL2AlhlxMpUUJWafj9+2y06ElolalmfKR+ggIfgIQ
|
||||
N1rXoDeScE4CFEj+wGg5cbma15STOdEFZVySD5HO+DPAC89J7uc5slpBhb5k2EvO
|
||||
dN0/HF7PQJa+AaPvvSJYsb+pVuEEuxSk3kF5SOPSuE3hhhrH5hDulI+Fk1JMDGcp
|
||||
vcH4qrWlnOiPWTHrHlplNy7rn2PqRQnY7HTFgyaWHPh2m00q24F0heSKvWvOF7rp
|
||||
t/aoVaWIQcAbS5N8nnMWZiMXXtb55FgIrSRREwIDAQABAoIBACxJObbA064+3xlh
|
||||
RRioSXx86+BIFwvUYLgAYSDacl3rvTFNcJRFLznteKDE1dPpXZqD6Zk3G8YEauce
|
||||
UD8nMj/awJs5+kVXSEC30E8/cmbYkE284E5J2OQVsunrvCM/skx2SD90aMhCdbm4
|
||||
B40h1EVwpOdH3alc3XIrTnNc0yK5MWAu41qwkxYxXHmW9Y0L8AjZve9JBrnKsJMB
|
||||
ETEZFhHgi/IWtfh5PLbJO2dbSe7Nqo4ikyWo3r5b3yvuphFz1il88ZLjJ5nDmtlH
|
||||
is7sk7pd0tYNsK1Di5G1ku50XvcbOE4F7mOVCxICTwjN+sdyG8o+AVlgbTKBo/JF
|
||||
uEhthCECgYEA/3YXS9mAEujlstrV4VOksYWtySSrLHC56tLjj8cHVPJ1qkzT4OOC
|
||||
X9TsWReDG4J8/t0DOHn+5dnhnqGcYjMMAQx095KHU1bQGrcRdmi6cjnNLTvfEbge
|
||||
IcJTYG5P7NpLfLjB3DOGqFR4o0iz4K9ZLTYJc+BaCB9qJBEw6nuoP+sCgYEAwBTN
|
||||
WpRDrmch0+LFPQwboLwtEPiFscTj8SInV0KsI/MK8+5Sm+tXS8PQHYJYcECEQxQM
|
||||
2gfyM8vy33UP4yn4edJGWlaz7a4hyDxn944vv2fBQ3vjJTNz3X3skkhZ2/F+ZW9e
|
||||
SFxPj+Vbif8VTEU+wK0f5SUmpRec4E7y3fq+kXkCgYEAib8ZbLLI1mlygfBx51/8
|
||||
rCRSwuTcz8ew2CgCwGInV+ys+bkXfmnuwNHE531AGrNPxvVRaUCO602C1NB7zI+N
|
||||
53raDyyZf5yN9fnElr592l3EfqGL9Lf8t2NbJeIVgrdqgMP29E9sSpPRwOnQ5FRo
|
||||
l3JNwoe0xDB8QRpr7+PhoyUCgYEAp+GGmmR7wzLgnhDV00WB4DqYKP0N3RH5KAhx
|
||||
2hKr4b/LEuh5y00mP1Il06TZJ0M8VmRv1yCa0CqxXB00hZdpVRAz7UFagaJwZFJn
|
||||
jDb6BJDqmdDt9tXBrxUgb7pMz6+CiaWNAjGsWFheaX5JXyAmeMDX369Y13KL6oEW
|
||||
RG2jogECgYEA/1vLZcWNK/0yd4ClU+Xbu8xC29q82JUMsaazHtbgSNlOfo9LMQlH
|
||||
z6xBiMYfHZ/SiHCy9RsO8GD4caXiF0RsTVnhqjSRJf3EARamufelNsu2ApLclkSN
|
||||
fzSoB7ZHddGaYKYpXkGzcwFcKd/QjAlHm1yIsZu4B52AhCxC/WS2X54=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
27
tests/ssl_certs/proxy_cert.key
Normal file
27
tests/ssl_certs/proxy_cert.key
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAzZO36nZvb9wLxBNB2cZyHqcX5poChJd1YnFBtxbtQwiISxid
|
||||
eGdiWImQE80vpUyTQbI7TxM+w1xZeEeu4PXuYrOgdTDRFEnjM2mteG+3WpHQBN4H
|
||||
xoah0msp3046fMkYqcEvhvHbsc5DAWgLK4JFHQPtG/+CIH0ZY+lBBPQhFIhBLYkt
|
||||
YxNVqwpsXOGreASSw6mO6cVehCuVFJQO5NnI1sCAvp3SeosMKeIcDZxpZWmZhSwH
|
||||
n3Rj6RMNM66C8zG4YlpvIniGzgV4UiW8XrTUG8HmzQ2295IcfB4No2DZeJDSR9oq
|
||||
jOkyqJXll+tSiAMuzBRtTQKvGZ5bpZWW4XELEQIDAQABAoIBAQCAfW2cjD4GimCI
|
||||
QwkLlq9JXWLg7S3ZtdjWmLdcOmY9WZ3mYhI6aVPcxs5Ysgyvonb/vui2+e5mqNf7
|
||||
B8LUNKK06lTGKqbjqXLqdYjJF/pgD3cXM7dkbE3EeNqJChogWIijwW11SMHqFmNn
|
||||
A6LHpPqRshyHPWIV8FroSagr8nKio5BjUEuUiQUUAmSJPGN5qUhdIWXcQu8R1JB8
|
||||
9qqqtwPR4FELbFVGI2vYHaSWGnf9V0boPOsfFXWbSq/Ksj3Lm3gAqMtlAeOFu84l
|
||||
fhP9RkgeXfaCXq0VaOM83UDgLqXm4Ni4wAMKRLwNs4LzumqMM/dfUTn+mGncj33q
|
||||
idp5qnDhAoGBAOXkwuf60F7aBbo98A0vWZli2CbkspsJz2J573pf+lVWI+ZHBZLI
|
||||
MOM2DgCOEIUfa2TIMkwFr2t9x6uXlACEwFbEtEBpM4J5qUHgGtXZIsnTsv3qUg/C
|
||||
L89cNrMddOuuRkxQbyK1QMYZZmZQjSKG2jW6m1KING+shtkOzQ/P9ildAoGBAOTs
|
||||
DLyyPeEZPj1UMqxVNmeYYRfWnt+YyTPulOIbSuFN0DhZPNLsjrhSxvDwe/3sYH/p
|
||||
nKdjnlFlx8frz9wtkCt0hWvY0pG2Zam4IBCvreFN7rSvpzHwUAK3oXic2TRKKu1m
|
||||
xUPZqMJwnWAPX+XxGFn0m7UJj+95VTEOJ2d12ClFAoGAdexXMgmM8uqg/3yf8xNz
|
||||
wWNbfu/W0gJBN8FWXw52aWmrNob9y+IWeaYTnqNAxBhuzR6H9kkAR4IYduNkzrNJ
|
||||
ufhigZu1CVuAv8LF4SXlW2PVL7wPZff08Efb4xrcC7y0YJbtuv8Af90tkpQFIU3N
|
||||
Brx2yeoGA7aa4SJfe5nwKh0CgYAo1yP+lh4MBqDf+CGCNUGbgcfwpM17PprGtQ3C
|
||||
uPPG9kbrhqAfUSy1Ha94VK8KQh2FNHxKMK+R/gKCXEOdGFPcLNGQyAHpFQ1WFg9C
|
||||
atUumOS5P40oj6L2mSQpjHIDrieyat9Ol4pQBh9Nf/Cv6S9a/RS6W5ZeNttIASpu
|
||||
fsutsQKBgQCq+BFeDYJH4f+C1233W3PXM0P1ivj+9TJMRUP63RRay6rv2ZTZXyPc
|
||||
Rx6Lv4OVWh9VMfv1kHRloJ1GKEBo/uD3nid1WqoNxpXv1iwxeGtjXkFHfvCB7Ruu
|
||||
vTyQhJQQ7WSCJJOfarstusIn0udOG3MLRgG4X1pPQghyS1AT8NUglw==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
30
tests/ssl_certs/ssl_cert_2.crt
Normal file
30
tests/ssl_certs/ssl_cert_2.crt
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIFOTCCA6GgAwIBAgIJAO+bbero+zKtMA0GCSqGSIb3DQEBCwUAMIGAMQswCQYD
|
||||
VQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsxETAPBgNVBAcMCEJyb29rbHluMQww
|
||||
CgYDVQQKDANOWVUxKTAnBgNVBAsMIENvbXB1dGVyIFNjaWVuY2UgYW5kIEVuZ2lu
|
||||
ZWVyaW5nMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTgwOTI2MTgwMDAzWhcNMzgw
|
||||
OTIxMTgwMDAzWjCBgDELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMREw
|
||||
DwYDVQQHDAhCcm9va2x5bjEMMAoGA1UECgwDTllVMSkwJwYDVQQLDCBDb21wdXRl
|
||||
ciBTY2llbmNlIGFuZCBFbmdpbmVlcmluZzESMBAGA1UEAwwJbG9jYWxob3N0MIIB
|
||||
ojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAxyFVeRsWnb1UlCKBks2azM9W
|
||||
9K+J/ZkzdSb6eCxOIxv79M/Ug54CfWqkySSaQejsu0U/gJxkFYRvwQAy5lATrspY
|
||||
2kyiWYiggWXFDWz+i8ETPkL9zn59v13sNIpT/IXQj0S3Mr9ZnsUn1qCyEOOIxJxZ
|
||||
lyuV/M/XP1DP4tArhEvrex12V6MQIK+8fYzEjHG/W7vIIet+wTStIR8ArvVQi0Kv
|
||||
PbbGCfrZ+e+gq+UpBLBuAfMzM95TW+YJ5duMchie2n6LDmOeegA4jMEv2ppeOr8Q
|
||||
JJtZuKpXWVbJvLg81yrDjr1rAwJR/WQrnk8GQWPCyPLneAA4mJbi75LqjLxn0AoJ
|
||||
b3kzLfGEMJJEWXspxNg06bLQU948hB4L7nKARq6s7KoESjEV+/L4koMPWJoNq6fx
|
||||
OUVw2+S3ITNrDctecRQ1j3RGVPaj5l6bn03C7KV9uRrfqFY3OUjn7A0kDczvRnmr
|
||||
e1BZIpe+mfGFB+Uu7JiQoBv6I6fqyrdH9rX1LUKlAgMBAAGjgbMwgbAwgZ8GA1Ud
|
||||
IwSBlzCBlKGBhqSBgzCBgDELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3Jr
|
||||
MREwDwYDVQQHDAhCcm9va2x5bjEMMAoGA1UECgwDTllVMSkwJwYDVQQLDCBDb21w
|
||||
dXRlciBTY2llbmNlIGFuZCBFbmdpbmVlcmluZzESMBAGA1UEAwwJbG9jYWxob3N0
|
||||
ggkA75tt6uj7Mq0wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAYEAFWcl
|
||||
1tAmt/3DJDjk0ppF62jbwcEOu1N9Nono9a70ojAQYYuMC7Ditw6rLbeXS8tP8ae/
|
||||
drlci3VxlE5PpmAjuP67Uv2CuGu/2iMqa99AWZ4mVN+x4YL6awvYs8ea6I1Xe8tQ
|
||||
5+RqvNA+QtnjtfOeb6yWQBAGrc2eTX87IzqvV/EewkdKAs4GZUWG1Zjv3effqjTO
|
||||
qRX94ltW1GWud7fVcqpZLOaK9U+4IaI2nNHuCtWODoyQmMoVApXyig/YQqFe0eyj
|
||||
76m1T+2SZLRtn0xn1fTHuLZ2bdtTMZ7k5PTAKnBNEn1Rr9MAS+WEASN1ZyoQ3reL
|
||||
VYrgkMTrrXPO8bdDTvP7z1Jzv5Cq9WMHFvOLfnj/vN9ZPH6w4QT3Zb97SAAOSPK/
|
||||
gzOzRtIe+hqCYBh/cwMoeeoAzes/nJgorj3IOTu8JXmtZrZGrdLIhu2Q8U+yKasf
|
||||
+TUrr6xdcJI/fyVM5BVelpGhqHzzOQe1tO4VYQlAVaaVvFidDPHqTI2/S272
|
||||
-----END CERTIFICATE-----
|
||||
30
tests/ssl_certs/ssl_cert_expired.crt
Normal file
30
tests/ssl_certs/ssl_cert_expired.crt
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIFOTCCA6GgAwIBAgIJALtyUsChEIJpMA0GCSqGSIb3DQEBCwUAMIGAMQswCQYD
|
||||
VQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsxETAPBgNVBAcMCEJyb29rbHluMQww
|
||||
CgYDVQQKDANOWVUxKTAnBgNVBAsMIENvbXB1dGVyIFNjaWVuY2UgYW5kIEVuZ2lu
|
||||
ZWVyaW5nMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTgwOTI2MTc0NTM2WhcNMTgw
|
||||
OTI1MTc0NTM2WjCBgDELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMREw
|
||||
DwYDVQQHDAhCcm9va2x5bjEMMAoGA1UECgwDTllVMSkwJwYDVQQLDCBDb21wdXRl
|
||||
ciBTY2llbmNlIGFuZCBFbmdpbmVlcmluZzESMBAGA1UEAwwJbG9jYWxob3N0MIIB
|
||||
ojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAxyFVeRsWnb1UlCKBks2azM9W
|
||||
9K+J/ZkzdSb6eCxOIxv79M/Ug54CfWqkySSaQejsu0U/gJxkFYRvwQAy5lATrspY
|
||||
2kyiWYiggWXFDWz+i8ETPkL9zn59v13sNIpT/IXQj0S3Mr9ZnsUn1qCyEOOIxJxZ
|
||||
lyuV/M/XP1DP4tArhEvrex12V6MQIK+8fYzEjHG/W7vIIet+wTStIR8ArvVQi0Kv
|
||||
PbbGCfrZ+e+gq+UpBLBuAfMzM95TW+YJ5duMchie2n6LDmOeegA4jMEv2ppeOr8Q
|
||||
JJtZuKpXWVbJvLg81yrDjr1rAwJR/WQrnk8GQWPCyPLneAA4mJbi75LqjLxn0AoJ
|
||||
b3kzLfGEMJJEWXspxNg06bLQU948hB4L7nKARq6s7KoESjEV+/L4koMPWJoNq6fx
|
||||
OUVw2+S3ITNrDctecRQ1j3RGVPaj5l6bn03C7KV9uRrfqFY3OUjn7A0kDczvRnmr
|
||||
e1BZIpe+mfGFB+Uu7JiQoBv6I6fqyrdH9rX1LUKlAgMBAAGjgbMwgbAwgZ8GA1Ud
|
||||
IwSBlzCBlKGBhqSBgzCBgDELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3Jr
|
||||
MREwDwYDVQQHDAhCcm9va2x5bjEMMAoGA1UECgwDTllVMSkwJwYDVQQLDCBDb21w
|
||||
dXRlciBTY2llbmNlIGFuZCBFbmdpbmVlcmluZzESMBAGA1UEAwwJbG9jYWxob3N0
|
||||
ggkAu3JSwKEQgmkwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAYEAW4I1
|
||||
TacdFv3L9ENFkSLciPb7zFMckLUZfk/P+4VjdapWrfuydO4W/ogMxA4DK09thTsK
|
||||
N/BgcExyKjDldGUfUv57Tqv3v2E5kbygNcNtP53fwMz3y+7QourzkDE5HWciw1Lb
|
||||
hmbnCBTzt/UioSBdJnAH29GWpSS+Jzu745sRaI48AS/J5ApH2aVEnNQTCE7v1LNH
|
||||
2bTTPYl3eDXiD8yOhvyiW1F4y2BSFbQRH/3aE6Goe4A75m8sX50+JlOgjyyQnAMf
|
||||
vbfvZsjGfqdXv9Qpci50qKCFxHJLXXNAUbX3fDgKE+RoZUNZnmn2VDgJYnToz6on
|
||||
RcVnppV09kmSjHXZBT04XXUA0vG3p+oU0TO4puJlePVf4Oz23/DRCPHSfVWgMeB2
|
||||
c1PpKit4+Bz7mypnsWVw8kk//l0GJ1cHnkkZElKJtPEB7I587jgTCDcN811TGNBc
|
||||
rLLd/JwtYAvi1CPFt2ICGDvA4AKLY3rBNg5z1DrSE/iom1NTC00SFZJztYiX
|
||||
-----END CERTIFICATE-----
|
||||
31
tests/ssl_certs/ssl_cert_wronghost.crt
Normal file
31
tests/ssl_certs/ssl_cert_wronghost.crt
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIFRTCCA62gAwIBAgIJAKY6b706lpuDMA0GCSqGSIb3DQEBCwUAMIGEMQswCQYD
|
||||
VQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsxETAPBgNVBAcMCEJyb29rbHluMQww
|
||||
CgYDVQQKDANOWVUxKTAnBgNVBAsMIENvbXB1dGVyIFNjaWVuY2UgYW5kIEVuZ2lu
|
||||
ZWVyaW5nMRYwFAYDVQQDDA1ub3RteWhvc3RuYW1lMB4XDTE4MDkxMjE2NTkxN1oX
|
||||
DTM4MDkwNzE2NTkxN1owgYQxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhOZXcgWW9y
|
||||
azERMA8GA1UEBwwIQnJvb2tseW4xDDAKBgNVBAoMA05ZVTEpMCcGA1UECwwgQ29t
|
||||
cHV0ZXIgU2NpZW5jZSBhbmQgRW5naW5lZXJpbmcxFjAUBgNVBAMMDW5vdG15aG9z
|
||||
dG5hbWUwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDHIVV5GxadvVSU
|
||||
IoGSzZrMz1b0r4n9mTN1Jvp4LE4jG/v0z9SDngJ9aqTJJJpB6Oy7RT+AnGQVhG/B
|
||||
ADLmUBOuyljaTKJZiKCBZcUNbP6LwRM+Qv3Ofn2/Xew0ilP8hdCPRLcyv1mexSfW
|
||||
oLIQ44jEnFmXK5X8z9c/UM/i0CuES+t7HXZXoxAgr7x9jMSMcb9bu8gh637BNK0h
|
||||
HwCu9VCLQq89tsYJ+tn576Cr5SkEsG4B8zMz3lNb5gnl24xyGJ7afosOY556ADiM
|
||||
wS/aml46vxAkm1m4qldZVsm8uDzXKsOOvWsDAlH9ZCueTwZBY8LI8ud4ADiYluLv
|
||||
kuqMvGfQCglveTMt8YQwkkRZeynE2DTpstBT3jyEHgvucoBGrqzsqgRKMRX78viS
|
||||
gw9Ymg2rp/E5RXDb5LchM2sNy15xFDWPdEZU9qPmXpufTcLspX25Gt+oVjc5SOfs
|
||||
DSQNzO9Geat7UFkil76Z8YUH5S7smJCgG/ojp+rKt0f2tfUtQqUCAwEAAaOBtzCB
|
||||
tDCBowYDVR0jBIGbMIGYoYGKpIGHMIGEMQswCQYDVQQGEwJVUzERMA8GA1UECAwI
|
||||
TmV3IFlvcmsxETAPBgNVBAcMCEJyb29rbHluMQwwCgYDVQQKDANOWVUxKTAnBgNV
|
||||
BAsMIENvbXB1dGVyIFNjaWVuY2UgYW5kIEVuZ2luZWVyaW5nMRYwFAYDVQQDDA1u
|
||||
b3RteWhvc3RuYW1lggkApjpvvTqWm4MwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0B
|
||||
AQsFAAOCAYEAvpBMce3kxwo9W0o4RqezkSxnNyax0ezbUNodIkx5kbzX09qQLqhK
|
||||
SkhQY3CNmtrpsczUg1W2nldxioEouwfTlhi15H98E/8XytpGaHO7Rnbtq8nkOp3E
|
||||
N1+DMfFR95OynbHSd7bfK9UEmH1CmCnttvCuQkLTxDCpEsQNAxvmU/yDONoDr+cu
|
||||
jGo80XTnYTqHl5/UtGbCS4SAIdWgrXTIqVvY/eF+mR+3nQEYjBuqW0cNfXLyYLXH
|
||||
XMc6qtfGX1P+NRWtlrWgGQmc0fry+GczRHMJuKtJMV2xZzPJAJqwwvj3Fjz8HNGu
|
||||
ZX3kVdbkDjf8is2cWgyZqDecqPHDBW4Ey539s/5eurgOkEvhriS4/9RnVhgdzduj
|
||||
nRdXkD10ficrFcBQO0KaTWT+iFBc9duuYPuLRyRTye5p3t0liOikH2XrRXs4IBfz
|
||||
2mT4npXQl1liNixcCf/yUEUOSQAJDG6aRjDjD4SZBUPDLjfqKLid8M0BpLQrks9L
|
||||
5hAg1WZXorY6
|
||||
-----END CERTIFICATE-----
|
||||
|
|
@ -21,6 +21,8 @@
|
|||
|
||||
NOTE: Make sure test_download.py is ran in 'tuf/tests/' directory.
|
||||
Otherwise, module that launches simple server would not be found.
|
||||
|
||||
TODO: Adopt the environment variable management from test_proxy_use.py here.
|
||||
"""
|
||||
|
||||
# Help with Python 3 compatibility, where the print statement is a function, an
|
||||
|
|
@ -36,6 +38,7 @@
|
|||
import os
|
||||
import random
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import unittest
|
||||
|
||||
|
|
@ -45,6 +48,8 @@
|
|||
import tuf.unittest_toolbox as unittest_toolbox
|
||||
import tuf.exceptions
|
||||
|
||||
import requests.exceptions
|
||||
|
||||
import securesystemslib
|
||||
import six
|
||||
|
||||
|
|
@ -69,8 +74,7 @@ def setUp(self):
|
|||
|
||||
# Launch a SimpleHTTPServer (serves files in the current dir).
|
||||
self.PORT = random.randint(30000, 45000)
|
||||
command = ['python', 'simple_server.py', str(self.PORT)]
|
||||
self.server_proc = subprocess.Popen(command, stderr=subprocess.PIPE)
|
||||
self.server_proc = popen_python(['simple_server.py', str(self.PORT)])
|
||||
logger.info('\n\tServer process started.')
|
||||
logger.info('\tServer process id: '+str(self.server_proc.pid))
|
||||
logger.info('\tServing on port: '+str(self.PORT))
|
||||
|
|
@ -168,81 +172,201 @@ def test_download_url_to_tempfileobj_and_urls(self):
|
|||
self.assertRaises(securesystemslib.exceptions.FormatError,
|
||||
download_file, None, self.target_data_length)
|
||||
|
||||
self.assertRaises(securesystemslib.exceptions.FormatError,
|
||||
self.assertRaises(tuf.exceptions.URLParsingError,
|
||||
download_file,
|
||||
self.random_string(), self.target_data_length)
|
||||
|
||||
self.assertRaises(six.moves.urllib.error.HTTPError,
|
||||
self.assertRaises(requests.exceptions.HTTPError,
|
||||
download_file,
|
||||
'http://localhost:' + str(self.PORT) + '/' + self.random_string(),
|
||||
self.target_data_length)
|
||||
|
||||
self.assertRaises(six.moves.urllib.error.URLError,
|
||||
self.assertRaises(requests.exceptions.ConnectionError,
|
||||
download_file,
|
||||
'http://localhost:' + str(self.PORT+1) + '/' + self.random_string(),
|
||||
self.target_data_length)
|
||||
|
||||
# Specify an unsupported URI scheme.
|
||||
url_with_unsupported_uri = self.url.replace('http', 'file')
|
||||
self.assertRaises(securesystemslib.exceptions.FormatError, download_file, url_with_unsupported_uri,
|
||||
self.assertRaises(requests.exceptions.InvalidSchema, download_file, url_with_unsupported_uri,
|
||||
self.target_data_length)
|
||||
self.assertRaises(securesystemslib.exceptions.FormatError, unsafe_download_file,
|
||||
self.assertRaises(requests.exceptions.InvalidSchema, unsafe_download_file,
|
||||
url_with_unsupported_uri, self.target_data_length)
|
||||
|
||||
|
||||
|
||||
def test__get_opener(self):
|
||||
# Test normal case.
|
||||
# A simple https server should be used to test the rest of the optional
|
||||
# ssl-related functions of 'tuf.download.py'.
|
||||
fake_cacert = self.make_temp_data_file()
|
||||
|
||||
with open(fake_cacert, 'wt') as file_object:
|
||||
file_object.write('fake cacert')
|
||||
|
||||
tuf.settings.ssl_certificates = fake_cacert
|
||||
tuf.download._get_opener('https')
|
||||
tuf.settings.ssl_certificates = None
|
||||
'''
|
||||
# This test uses sites on the internet, requiring a net connection to succeed.
|
||||
# Since this is the only such test in TUF, I'm not going to enable it... but
|
||||
# it's here in case it's useful for diagnosis.
|
||||
def test_https_validation(self):
|
||||
"""
|
||||
Use some known URLs on the net to ensure that TUF download checks SSL
|
||||
certificates appropriately.
|
||||
"""
|
||||
# We should never get as far as the target file download itself, so the
|
||||
# length we pass to safe_download and unsafe_download shouldn't matter.
|
||||
irrelevant_length = 10
|
||||
|
||||
for bad_url in [
|
||||
'https://expired.badssl.com/', # expired certificate
|
||||
'https://wrong.host.badssl.com/', ]: # hostname verification fail
|
||||
|
||||
with self.assertRaises(requests.exceptions.SSLError):
|
||||
download.safe_download(bad_url, irrelevant_length)
|
||||
|
||||
with self.assertRaises(requests.exceptions.SSLError):
|
||||
download.unsafe_download(bad_url, irrelevant_length)
|
||||
'''
|
||||
|
||||
|
||||
|
||||
|
||||
def test_https_connection(self):
|
||||
"""
|
||||
Try various HTTPS downloads using trusted and untrusted certificates with
|
||||
and without the correct hostname listed in the SSL certificate.
|
||||
"""
|
||||
# Make a temporary file to be served to the client.
|
||||
current_directory = os.getcwd()
|
||||
target_filepath = self.make_temp_data_file(directory=current_directory)
|
||||
target_data = None
|
||||
target_data_length = 0
|
||||
|
||||
with open(target_filepath, 'r') as target_file_object:
|
||||
target_data = target_file_object.read()
|
||||
target_data_length = len(target_data)
|
||||
target_data_length = len(target_file_object.read())
|
||||
|
||||
# Launch an https server (serves files in the current dir).
|
||||
port = random.randint(30000, 45000)
|
||||
command = ['python', 'simple_https_server.py', str(port)]
|
||||
https_server_process = subprocess.Popen(command, stderr=subprocess.PIPE)
|
||||
# These cert files provide various test cases:
|
||||
# good: A valid cert from an older generation of test_download.py tests.
|
||||
# good2: A valid cert made simultaneous to the bad certs below, with the
|
||||
# same settings otherwise, tested here in case the difference
|
||||
# between the way the new bad certs and the old good cert were
|
||||
# generated turns out to matter at some point.
|
||||
# bad: An otherwise-valid cert with the wrong hostname. The good certs
|
||||
# list "localhost", but this lists "notmyhostname".
|
||||
# expired: An otherwise-valid cert but which is expired (no valid dates
|
||||
# exist, fwiw: startdate > enddate).
|
||||
good_cert_fname = os.path.join('ssl_certs', 'ssl_cert.crt')
|
||||
good2_cert_fname = os.path.join('ssl_certs', 'ssl_cert_2.crt')
|
||||
bad_cert_fname = os.path.join('ssl_certs', 'ssl_cert_wronghost.crt')
|
||||
expired_cert_fname = os.path.join('ssl_certs', 'ssl_cert_expired.crt')
|
||||
|
||||
# NOTE: Following error is raised if delay is not applied:
|
||||
# <urlopen error [Errno 111] Connection refused>
|
||||
time.sleep(1)
|
||||
# Launch four HTTPS servers (serve files in the current dir).
|
||||
# 1: we expect to operate correctly
|
||||
# 2: also good; uses a slightly different cert (controls for the cert
|
||||
# generation method used for the next two, in case it comes to matter)
|
||||
# 3: run with an HTTPS certificate with an unexpected hostname
|
||||
# 4: run with an HTTPS certificate that is expired
|
||||
port1 = str(random.randint(30000, 45000))
|
||||
port2 = str(int(port1) + 1)
|
||||
port3 = str(int(port1) + 2)
|
||||
port4 = str(int(port1) + 3)
|
||||
good_https_server_proc = popen_python(
|
||||
['simple_https_server.py', port1, good_cert_fname])
|
||||
good2_https_server_proc = popen_python(
|
||||
['simple_https_server.py', port2, good2_cert_fname])
|
||||
bad_https_server_proc = popen_python(
|
||||
['simple_https_server.py', port3, bad_cert_fname])
|
||||
expd_https_server_proc = popen_python(
|
||||
['simple_https_server.py', port4, expired_cert_fname])
|
||||
|
||||
junk, relative_target_filepath = os.path.split(target_filepath)
|
||||
https_url = 'https://localhost:' + str(port) + '/' + relative_target_filepath
|
||||
# Provide a delay long enough to allow the HTTPS servers to start.
|
||||
# Encountered an error on one test system at delay value of 0.2s, so
|
||||
# increasing to 0.5s.
|
||||
# Expect to see "Connection refused" if this delay is not long enough
|
||||
# (though other issues could cause that).
|
||||
time.sleep(0.5)
|
||||
|
||||
# Download the target file using an https connection.
|
||||
tuf.settings.ssl_certificates = 'ssl_cert.crt'
|
||||
message = 'Downloading target file from https server: ' + https_url
|
||||
logger.info(message)
|
||||
relative_target_fpath = os.path.basename(target_filepath)
|
||||
good_https_url = 'https://localhost:' + port1 + '/' + relative_target_fpath
|
||||
good2_https_url = good_https_url.replace(':' + port1, ':' + port2)
|
||||
bad_https_url = good_https_url.replace(':' + port1, ':' + port3)
|
||||
expired_https_url = good_https_url.replace(':' + port1, ':' + port4)
|
||||
|
||||
# Download the target file using an HTTPS connection.
|
||||
|
||||
# Use try-finally solely to ensure that the server processes are killed.
|
||||
try:
|
||||
download.safe_download(https_url, target_data_length)
|
||||
download.unsafe_download(https_url, target_data_length)
|
||||
# Trust the certfile that happens to use a different hostname than we
|
||||
# will expect.
|
||||
os.environ['REQUESTS_CA_BUNDLE'] = bad_cert_fname
|
||||
# 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 = {}
|
||||
|
||||
# Try connecting to the server process with the bad cert while trusting
|
||||
# the bad cert. Expect failure because even though we trust it, the
|
||||
# hostname we're connecting to does not match the hostname in the cert.
|
||||
logger.info('Trying HTTPS download of target file: ' + bad_https_url)
|
||||
with self.assertRaises(requests.exceptions.SSLError):
|
||||
download.safe_download(bad_https_url, target_data_length)
|
||||
with self.assertRaises(requests.exceptions.SSLError):
|
||||
download.unsafe_download(bad_https_url, target_data_length)
|
||||
|
||||
# Try connecting to the server processes with the good certs while not
|
||||
# trusting the good certs (trusting the bad cert instead). Expect failure
|
||||
# because even though the server's cert file is otherwise OK, we don't
|
||||
# trust it.
|
||||
print('Trying HTTPS download of target file: ' + good_https_url)
|
||||
with self.assertRaises(requests.exceptions.SSLError):
|
||||
download.safe_download(good_https_url, target_data_length)
|
||||
with self.assertRaises(requests.exceptions.SSLError):
|
||||
download.unsafe_download(good_https_url, target_data_length)
|
||||
|
||||
print('Trying HTTPS download of target file: ' + good2_https_url)
|
||||
with self.assertRaises(requests.exceptions.SSLError):
|
||||
download.safe_download(good2_https_url, target_data_length)
|
||||
with self.assertRaises(requests.exceptions.SSLError):
|
||||
download.unsafe_download(good2_https_url, target_data_length)
|
||||
|
||||
|
||||
# Configure environment to now trust the certfile that is expired.
|
||||
os.environ['REQUESTS_CA_BUNDLE'] = expired_cert_fname
|
||||
# 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 = {}
|
||||
|
||||
# Try connecting to the server process with the expired cert while
|
||||
# trusting the expired cert. Expect failure because even though we trust
|
||||
# it, it is expired.
|
||||
logger.info('Trying HTTPS download of target file: ' + expired_https_url)
|
||||
with self.assertRaises(requests.exceptions.SSLError):
|
||||
download.safe_download(expired_https_url, target_data_length)
|
||||
with self.assertRaises(requests.exceptions.SSLError):
|
||||
download.unsafe_download(expired_https_url, target_data_length)
|
||||
|
||||
|
||||
# Try connecting to the server processes with the good certs while
|
||||
# trusting the appropriate good certs. Expect success.
|
||||
# TODO: expand testing to switch expected certificates back and forth a
|
||||
# bit more while clearing / not clearing sessions.
|
||||
os.environ['REQUESTS_CA_BUNDLE'] = good_cert_fname
|
||||
# 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 of target file: ' + good_https_url)
|
||||
download.safe_download(good_https_url, target_data_length)
|
||||
download.unsafe_download(good_https_url, target_data_length)
|
||||
|
||||
os.environ['REQUESTS_CA_BUNDLE'] = good2_cert_fname
|
||||
# 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 of target file: ' + good2_https_url)
|
||||
download.safe_download(good2_https_url, target_data_length)
|
||||
download.unsafe_download(good2_https_url, target_data_length)
|
||||
|
||||
finally:
|
||||
if https_server_process.returncode is None:
|
||||
message = \
|
||||
'Server process ' + str(https_server_process.pid) + ' terminated.'
|
||||
logger.info(message)
|
||||
https_server_process.kill()
|
||||
for proc in [
|
||||
good_https_server_proc,
|
||||
good2_https_server_proc,
|
||||
bad_https_server_proc,
|
||||
expd_https_server_proc]:
|
||||
if proc.returncode is None:
|
||||
logger.info('Terminating server process ' + str(proc.pid))
|
||||
proc.kill()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -252,6 +376,27 @@ def test__get_content_length(self):
|
|||
self.assertEqual(content_length, None)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# TODO: Move this to a common test module (tests/common.py?)
|
||||
# and strip it test_proxy_use.py and test_download.py.
|
||||
def popen_python(command_arg_list):
|
||||
"""
|
||||
Run subprocess.Popen() to produce a process running a Python interpreter.
|
||||
Uses the same Python interpreter that the current process is using, via
|
||||
sys.executable.
|
||||
"""
|
||||
assert sys.executable, 'Test cannot function: unable to determine ' \
|
||||
'current Python interpreter via sys.executable.'
|
||||
|
||||
return subprocess.Popen(
|
||||
[sys.executable] + command_arg_list, stderr=subprocess.PIPE)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Run unit test.
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
|
|||
385
tests/test_proxy_use.py
Normal file
385
tests/test_proxy_use.py
Normal file
|
|
@ -0,0 +1,385 @@
|
|||
#!/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 Python3.6+, as it uses mitmproxy, which only supports
|
||||
Python3.6+.
|
||||
|
||||
So long as the tests succeed in Python 3.6+, it is unlikely that TUF
|
||||
behaves differently with respect to proxies when it runs in other Python
|
||||
versions.
|
||||
|
||||
As a result of this dependency on a modern Python version, this test does
|
||||
not run with the other unit tests, being specifically excluded from
|
||||
aggregate_tests.py.
|
||||
"""
|
||||
|
||||
# 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 hashlib
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import unittest
|
||||
|
||||
import tuf
|
||||
import tuf.download as download
|
||||
import tuf.log
|
||||
import tuf.unittest_toolbox as unittest_toolbox
|
||||
import tuf.exceptions
|
||||
|
||||
import requests.exceptions
|
||||
|
||||
import securesystemslib
|
||||
import six
|
||||
|
||||
logger = logging.getLogger('tuf.test_download')
|
||||
|
||||
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()
|
||||
|
||||
# Launch a simple HTTP server (serves files in the current dir).
|
||||
cls.http_port = random.randint(30000, 45000)
|
||||
cls.http_server_proc = popen_python(
|
||||
['simple_server.py', str(cls.http_port)])
|
||||
|
||||
# Launch an HTTPS server (serves files in the current dir).
|
||||
cls.https_port = cls.http_port + 1
|
||||
cls.https_server_proc = popen_python(
|
||||
['simple_https_server.py', str(cls.https_port)])
|
||||
|
||||
|
||||
# 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_port = cls.http_port + 2
|
||||
cls.http_proxy_proc = popen_python(
|
||||
['proxy_server.py', str(cls.http_proxy_port)])
|
||||
# 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_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.
|
||||
cls.https_proxy_port = cls.http_port + 3
|
||||
cls.https_proxy_proc = popen_python(
|
||||
['proxy_server.py', str(cls.https_proxy_port), 'intercept',
|
||||
os.path.join('ssl_certs', 'ssl_cert.crt')])
|
||||
# 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://127.0.0.1:' + str(cls.https_proxy_port)
|
||||
|
||||
# Give the HTTP server and proxy server processes a little bit of time to
|
||||
# start listening before allowing tests to begin, lest we get "Connection
|
||||
# refused" errors. On the first test system. 0.1s was too short and 0.15s
|
||||
# was long enough. Use 0.5s to be safe, and if issues arise, increase it.
|
||||
# Observed some occasional AppVeyor failures, so increasing this to 1s.
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@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 in [
|
||||
cls.http_server_proc,
|
||||
cls.https_server_proc,
|
||||
cls.http_proxy_proc,
|
||||
cls.https_proxy_proc,
|
||||
]:
|
||||
if proc.returncode is None:
|
||||
logger.info('\tTerminating process ' + str(proc.pid) + ' in cleanup.')
|
||||
proc.kill()
|
||||
|
||||
|
||||
|
||||
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)
|
||||
rel_target_filepath = os.path.basename(target_filepath)
|
||||
|
||||
with open(target_filepath, 'r') as target_file_object:
|
||||
self.target_data_length = len(target_file_object.read())
|
||||
|
||||
self.url = \
|
||||
'http://localhost:' + str(self.http_port) + '/' + rel_target_filepath
|
||||
|
||||
self.url_https = \
|
||||
'https://localhost:' + str(self.https_port) + '/' + rel_target_filepath
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# TODO: Move this to a common test module (tests/common.py?)
|
||||
# and strip it test_proxy_use.py and test_download.py.
|
||||
def popen_python(command_arg_list):
|
||||
"""
|
||||
Run subprocess.Popen() to produce a process running a Python interpreter.
|
||||
Uses the same Python interpreter that the current process is using, via
|
||||
sys.executable.
|
||||
"""
|
||||
assert sys.executable, 'Test cannot function: unable to determine ' \
|
||||
'current Python interpreter via sys.executable.'
|
||||
|
||||
return subprocess.Popen(
|
||||
[sys.executable] + command_arg_list, stderr=subprocess.PIPE)
|
||||
|
||||
|
||||
|
||||
# Run unit test.
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
@ -31,6 +31,9 @@
|
|||
does not fall below a required rate (tuf.settings.MIN_AVERAGE_DOWNLOAD_SPEED).
|
||||
|
||||
Note: There is no difference between 'updates' and 'target' files.
|
||||
|
||||
# TODO: Consider additional tests for slow metadata download. Tests here only
|
||||
use slow target download.
|
||||
"""
|
||||
|
||||
# Help with Python 3 compatibility, where the print statement is a function, an
|
||||
|
|
@ -154,13 +157,20 @@ def setUp(self):
|
|||
shutil.copytree(original_client, self.client_directory)
|
||||
shutil.copytree(original_keystore, self.keystore_directory)
|
||||
|
||||
|
||||
# Produce a longer target file than exists in the other test repository
|
||||
# data, to provide for a long-duration slow attack. Then we'll write new
|
||||
# top-level metadata that includes a hash over that file, and provide that
|
||||
# metadata to the client as well.
|
||||
|
||||
# The slow retrieval server, in mode 2 (1 byte per second), will only
|
||||
# sleep for a total of (target file size) seconds. Add a target file
|
||||
# that contains sufficient number of bytes to trigger a slow retrieval
|
||||
# error. "sufficient number of bytes" assumed to be
|
||||
# >> 'tuf.settings.SLOW_START_GRACE_PERIOD' bytes.
|
||||
extra_bytes = 8
|
||||
total_bytes = tuf.settings.SLOW_START_GRACE_PERIOD + extra_bytes
|
||||
# error. A transfer should not be permitted to take 1 second per byte
|
||||
# transferred. Because this test is currently expected to fail, I'm
|
||||
# limiting the size to 10 bytes (10 seconds) to avoid expected testing
|
||||
# delays.... Consider increasing again after fix, to, e.g. 400.
|
||||
total_bytes = 10
|
||||
|
||||
repository = repo_tool.load_repository(self.repository_directory)
|
||||
file1_filepath = os.path.join(self.repository_directory, 'targets',
|
||||
|
|
@ -190,6 +200,19 @@ def setUp(self):
|
|||
shutil.copytree(os.path.join(self.repository_directory, 'metadata.staged'),
|
||||
os.path.join(self.repository_directory, 'metadata'))
|
||||
|
||||
# Since we've changed the repository metadata in this setup (by lengthening
|
||||
# a target file and then writing new metadata), we also have to update the
|
||||
# client metadata to get to the expected initial state, where the client
|
||||
# knows the right target info (and so expects the right, longer target
|
||||
# length.
|
||||
# We'll skip using updater.refresh since we don't have a server running,
|
||||
# and we'll update the metadata locally, manually.
|
||||
shutil.rmtree(os.path.join(
|
||||
self.client_directory, self.repository_name, 'metadata', 'current'))
|
||||
shutil.copytree(os.path.join(self.repository_directory, 'metadata'),
|
||||
os.path.join(self.client_directory, self.repository_name, 'metadata',
|
||||
'current'))
|
||||
|
||||
# Set the url prefix required by the 'tuf/client/updater.py' updater.
|
||||
# 'path/to/tmp/repository' -> 'localhost:8001/tmp/repository'.
|
||||
repository_basepath = self.repository_directory[len(os.getcwd()):]
|
||||
|
|
@ -252,6 +275,13 @@ def test_with_tuf_mode_1(self):
|
|||
|
||||
|
||||
|
||||
# The following test fails as a result of a change to TUF's download code.
|
||||
# Rather than constructing urllib2 requests, we now use the requests library.
|
||||
# This solves an HTTPS proxy issue, but has for the moment deprived us of a
|
||||
# way to prevent certain this kind of slow retrieval attack.
|
||||
# See conversation in PR: https://github.com/theupdateframework/tuf/pull/781
|
||||
# TODO: Update download code to resolve the slow retrieval vulnerability.
|
||||
@unittest.expectedFailure
|
||||
def test_with_tuf_mode_2(self):
|
||||
# Simulate a slow retrieval attack.
|
||||
# 'mode_2': During the download process, the server blocks the download
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
# This value is used in the requests user agent.
|
||||
# setup.py has it hard-coded separately.
|
||||
# Currently, when the version is changed, it must be set in both locations.
|
||||
# TODO: Single-source the version number.
|
||||
__version__ = "0.11.1"
|
||||
351
tuf/download.py
351
tuf/download.py
|
|
@ -34,30 +34,37 @@
|
|||
from __future__ import division
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import socket
|
||||
import logging
|
||||
import time
|
||||
import timeit
|
||||
import ssl
|
||||
|
||||
import tuf
|
||||
import requests
|
||||
|
||||
import securesystemslib
|
||||
import securesystemslib.util
|
||||
import six
|
||||
import tuf.exceptions
|
||||
|
||||
# 'ssl.match_hostname' was added in Python 3.2. The vendored version is needed
|
||||
# for Python 2.7.
|
||||
try:
|
||||
from ssl import match_hostname
|
||||
|
||||
except ImportError: # pragma: no cover
|
||||
from securesystemslib._vendor.ssl_match_hostname import match_hostname
|
||||
import urllib3.exceptions
|
||||
|
||||
# See 'log.py' to learn how logging is handled in TUF.
|
||||
logger = logging.getLogger('tuf.download')
|
||||
|
||||
# From http://docs.python-requests.org/en/master/user/advanced/#session-objects:
|
||||
#
|
||||
# "The Session object allows you to persist certain parameters across requests.
|
||||
# It also persists cookies across all requests made from the Session instance,
|
||||
# and will use urllib3's connection pooling. So if you're making several
|
||||
# requests to the same host, the underlying TCP connection will be reused,
|
||||
# which can result in a significant performance increase (see HTTP persistent
|
||||
# connection)."
|
||||
#
|
||||
# NOTE: We use a separate requests.Session per scheme+hostname combination, in
|
||||
# order to reuse connections to the same hostname to improve efficiency, but
|
||||
# avoiding sharing state between different hosts-scheme combinations to
|
||||
# minimize subtle security issues. Some cookies may not be HTTP-safe.
|
||||
_sessions = {}
|
||||
|
||||
|
||||
def safe_download(url, required_length):
|
||||
|
|
@ -76,8 +83,7 @@ def safe_download(url, required_length):
|
|||
|
||||
<Arguments>
|
||||
url:
|
||||
A URL string that represents the location of the file. The URI scheme
|
||||
component must be one of 'tuf.settings.SUPPORTED_URI_SCHEMES'.
|
||||
A URL string that represents the location of the file.
|
||||
|
||||
required_length:
|
||||
An integer value representing the length of the file. This is an exact
|
||||
|
|
@ -106,20 +112,6 @@ def safe_download(url, required_length):
|
|||
securesystemslib.formats.URL_SCHEMA.check_match(url)
|
||||
securesystemslib.formats.LENGTH_SCHEMA.check_match(required_length)
|
||||
|
||||
# Ensure 'url' specifies one of the URI schemes in
|
||||
# 'tuf.settings.SUPPORTED_URI_SCHEMES'. Be default, ['http', 'https'] is
|
||||
# supported. If the URI scheme of 'url' is empty or "file", files on the
|
||||
# local system can be accessed. Unexpected files may be accessed by
|
||||
# compromised metadata (unlikely to happen if targets.json metadata is signed
|
||||
# with offline keys).
|
||||
parsed_url = six.moves.urllib.parse.urlparse(url)
|
||||
|
||||
if parsed_url.scheme not in tuf.settings.SUPPORTED_URI_SCHEMES:
|
||||
message = \
|
||||
repr(url) + ' specifies an unsupported URI scheme. Supported ' + \
|
||||
' URI Schemes: ' + repr(tuf.settings.SUPPORTED_URI_SCHEMES)
|
||||
raise securesystemslib.exceptions.FormatError(message)
|
||||
|
||||
return _download_file(url, required_length, STRICT_REQUIRED_LENGTH=True)
|
||||
|
||||
|
||||
|
|
@ -142,8 +134,7 @@ def unsafe_download(url, required_length):
|
|||
|
||||
<Arguments>
|
||||
url:
|
||||
A URL string that represents the location of the file. The URI scheme
|
||||
component must be one of 'tuf.settings.SUPPORTED_URI_SCHEMES'.
|
||||
A URL string that represents the location of the file.
|
||||
|
||||
required_length:
|
||||
An integer value representing the length of the file. This is an upper
|
||||
|
|
@ -172,20 +163,6 @@ def unsafe_download(url, required_length):
|
|||
securesystemslib.formats.URL_SCHEMA.check_match(url)
|
||||
securesystemslib.formats.LENGTH_SCHEMA.check_match(required_length)
|
||||
|
||||
# Ensure 'url' specifies one of the URI schemes in
|
||||
# 'tuf.settings.SUPPORTED_URI_SCHEMES'. Be default, ['http', 'https'] is
|
||||
# supported. If the URI scheme of 'url' is empty or "file", files on the
|
||||
# local system can be accessed. Unexpected files may be accessed by
|
||||
# compromised metadata (unlikely to happen if targets.json metadata is signed
|
||||
# with offline keys).
|
||||
parsed_url = six.moves.urllib.parse.urlparse(url)
|
||||
|
||||
if parsed_url.scheme not in tuf.settings.SUPPORTED_URI_SCHEMES:
|
||||
message = \
|
||||
repr(url) + ' specifies an unsupported URI scheme. Supported ' + \
|
||||
' URI Schemes: ' + repr(tuf.settings.SUPPORTED_URI_SCHEMES)
|
||||
raise securesystemslib.exceptions.FormatError(message)
|
||||
|
||||
return _download_file(url, required_length, STRICT_REQUIRED_LENGTH=False)
|
||||
|
||||
|
||||
|
|
@ -229,9 +206,6 @@ def _download_file(url, required_length, STRICT_REQUIRED_LENGTH=True):
|
|||
securesystemslib.exceptions.FormatError, if any of the arguments are
|
||||
improperly formatted.
|
||||
|
||||
tuf.exceptions.Error, if the certificates pointed to by
|
||||
tuf.settings.ssl_certificates cannot be loaded.
|
||||
|
||||
Any other unforeseen runtime exception.
|
||||
|
||||
<Returns>
|
||||
|
|
@ -257,14 +231,62 @@ def _download_file(url, required_length, STRICT_REQUIRED_LENGTH=True):
|
|||
temp_file = securesystemslib.util.TempFile()
|
||||
|
||||
try:
|
||||
# Open the connection to the remote file. _open_connection() can raise
|
||||
# socket connection exceptions (such as SSLError). Connection errors of
|
||||
# this kind can be minimized by adjusting the socket timeout in
|
||||
# settings.py.
|
||||
connection = _open_connection(url)
|
||||
# Use a different requests.Session per schema+hostname combination, to
|
||||
# reuse connections while minimizing subtle security issues.
|
||||
parsed_url = six.moves.urllib.parse.urlparse(url)
|
||||
|
||||
if not parsed_url.scheme or not parsed_url.hostname:
|
||||
raise tuf.exceptions.URLParsingError(
|
||||
'Could not get scheme and hostname from URL: ' + url)
|
||||
|
||||
session_index = parsed_url.scheme + '+' + parsed_url.hostname
|
||||
|
||||
logger.debug('url: ' + url)
|
||||
logger.debug('session index: ' + session_index)
|
||||
|
||||
session = _sessions.get(session_index)
|
||||
|
||||
if not session:
|
||||
session = requests.Session()
|
||||
_sessions[session_index] = session
|
||||
|
||||
# Attach some default headers to every Session.
|
||||
requests_user_agent = session.headers['User-Agent']
|
||||
# Follows the RFC: https://tools.ietf.org/html/rfc7231#section-5.5.3
|
||||
tuf_user_agent = 'tuf/' + tuf.__version__ + ' ' + requests_user_agent
|
||||
session.headers.update({
|
||||
# Tell the server not to compress or modify anything.
|
||||
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding#Directives
|
||||
'Accept-Encoding': 'identity',
|
||||
# The TUF user agent.
|
||||
'User-Agent': tuf_user_agent})
|
||||
|
||||
logger.debug('Made new session for ' + session_index)
|
||||
|
||||
else:
|
||||
logger.debug('Reusing session for ' + session_index)
|
||||
|
||||
# Get the requests.Response object for this URL.
|
||||
#
|
||||
# Always stream to control how requests are downloaded:
|
||||
# http://docs.python-requests.org/en/master/user/advanced/#body-content-workflow
|
||||
#
|
||||
# We will always manually close Responses, so no need for a context
|
||||
# manager.
|
||||
#
|
||||
# Always set the timeout. This timeout value is interpreted by requests as:
|
||||
# - connect timeout (max delay before first byte is received)
|
||||
# - read (gap) timeout (max delay between bytes received)
|
||||
# These are NOT overall/total, wall-clock timeouts for any single read.
|
||||
# http://docs.python-requests.org/en/master/user/advanced/#timeouts
|
||||
response = session.get(
|
||||
url, stream=True, timeout=tuf.settings.SOCKET_TIMEOUT)
|
||||
|
||||
# Check response status.
|
||||
response.raise_for_status()
|
||||
|
||||
# We ask the server about how big it thinks this file should be.
|
||||
reported_length = _get_content_length(connection)
|
||||
reported_length = _get_content_length(response)
|
||||
|
||||
# Then, we check whether the required length matches the reported length.
|
||||
_check_content_length(reported_length, required_length,
|
||||
|
|
@ -273,7 +295,7 @@ def _download_file(url, required_length, STRICT_REQUIRED_LENGTH=True):
|
|||
# Download the contents of the URL, up to the required length, to a
|
||||
# temporary file, and get the total number of downloaded bytes.
|
||||
total_downloaded, average_download_speed = \
|
||||
_download_fixed_amount_of_data(connection, temp_file, required_length)
|
||||
_download_fixed_amount_of_data(response, temp_file, required_length)
|
||||
|
||||
# Does the total number of downloaded bytes match the required length?
|
||||
_check_downloaded_length(total_downloaded, required_length,
|
||||
|
|
@ -293,21 +315,20 @@ def _download_file(url, required_length, STRICT_REQUIRED_LENGTH=True):
|
|||
|
||||
|
||||
|
||||
def _download_fixed_amount_of_data(connection, temp_file, required_length):
|
||||
def _download_fixed_amount_of_data(response, temp_file, required_length):
|
||||
"""
|
||||
<Purpose>
|
||||
This is a helper function, where the download really happens. While-block
|
||||
reads data from connection a fixed chunk of data at a time, or less, until
|
||||
reads data from response a fixed chunk of data at a time, or less, until
|
||||
'required_length' is reached.
|
||||
|
||||
<Arguments>
|
||||
connection:
|
||||
The object that the _open_connection returns for communicating with the
|
||||
server about the contents of a URL.
|
||||
response:
|
||||
The object for communicating with the server about the contents of a URL.
|
||||
|
||||
temp_file:
|
||||
A temporary file where the contents at the URL specified by the
|
||||
'connection' object will be stored.
|
||||
'response' object will be stored.
|
||||
|
||||
required_length:
|
||||
The number of bytes that we must download for the file. This is almost
|
||||
|
|
@ -319,7 +340,11 @@ def _download_fixed_amount_of_data(connection, temp_file, required_length):
|
|||
Data from the server will be written to 'temp_file'.
|
||||
|
||||
<Exceptions>
|
||||
Runtime or network exceptions will be raised without question.
|
||||
tuf.exceptions.SlowRetrievalError
|
||||
will be raised if urllib3.exceptions.ReadTimeoutError is caught (if the
|
||||
download times out).
|
||||
|
||||
Otherwise, runtime or network exceptions will be raised without question.
|
||||
|
||||
<Returns>
|
||||
A (total_downloaded, average_download_speed) tuple, where
|
||||
|
|
@ -328,12 +353,6 @@ def _download_fixed_amount_of_data(connection, temp_file, required_length):
|
|||
attempt.
|
||||
"""
|
||||
|
||||
# Tolerate servers with a slow start by ignoring their delivery speed for
|
||||
# 'tuf.settings.SLOW_START_GRACE_PERIOD' seconds. Set 'seconds_spent_receiving'
|
||||
# to negative SLOW_START_GRACE_PERIOD seconds, and begin checking the average
|
||||
# download speed once it is positive.
|
||||
grace_period = -tuf.settings.SLOW_START_GRACE_PERIOD
|
||||
|
||||
# Keep track of total bytes downloaded.
|
||||
number_of_bytes_received = 0
|
||||
average_download_speed = 0
|
||||
|
|
@ -350,20 +369,17 @@ def _download_fixed_amount_of_data(connection, temp_file, required_length):
|
|||
if tuf.settings.SLEEP_BEFORE_ROUND:
|
||||
time.sleep(tuf.settings.SLEEP_BEFORE_ROUND)
|
||||
|
||||
data = b''
|
||||
read_amount = min(tuf.settings.CHUNK_SIZE,
|
||||
required_length - number_of_bytes_received)
|
||||
read_amount = min(
|
||||
tuf.settings.CHUNK_SIZE, required_length - number_of_bytes_received)
|
||||
|
||||
try:
|
||||
data = connection.read(read_amount)
|
||||
|
||||
# Python 3.2 returns 'IOError' if the remote file object has timed out.
|
||||
except (socket.error, IOError):
|
||||
pass
|
||||
# NOTE: This may not handle some servers adding a Content-Encoding
|
||||
# header, which may cause urllib3 to misbehave:
|
||||
# https://github.com/pypa/pip/blob/404838abcca467648180b358598c597b74d568c9/src/pip/_internal/download.py#L547-L582
|
||||
data = response.raw.read(read_amount)
|
||||
|
||||
number_of_bytes_received = number_of_bytes_received + len(data)
|
||||
|
||||
# Data successfully read from the connection. Store it.
|
||||
# Data successfully read from the response. Store it.
|
||||
temp_file.write(data)
|
||||
|
||||
if number_of_bytes_received == required_length:
|
||||
|
|
@ -372,9 +388,6 @@ def _download_fixed_amount_of_data(connection, temp_file, required_length):
|
|||
stop_time = timeit.default_timer()
|
||||
seconds_spent_receiving = stop_time - start_time
|
||||
|
||||
if (seconds_spent_receiving + grace_period) < 0:
|
||||
continue
|
||||
|
||||
# Measure the average download speed.
|
||||
average_download_speed = number_of_bytes_received / seconds_spent_receiving
|
||||
|
||||
|
|
@ -395,121 +408,31 @@ def _download_fixed_amount_of_data(connection, temp_file, required_length):
|
|||
# Finally, we signal that the download is complete.
|
||||
break
|
||||
|
||||
except urllib3.exceptions.ReadTimeoutError as e:
|
||||
# Whatever happens, make sure that we always close the connection.
|
||||
response.close()
|
||||
raise tuf.exceptions.SlowRetrievalError(str(e))
|
||||
|
||||
except:
|
||||
# Whatever happens, make sure that we always close the connection.
|
||||
connection.close()
|
||||
response.close()
|
||||
raise
|
||||
|
||||
connection.close()
|
||||
response.close()
|
||||
return number_of_bytes_received, average_download_speed
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def _get_request(url):
|
||||
"""
|
||||
Wraps the URL to retrieve to protects against "creative"
|
||||
interpretation of the RFC: http://bugs.python.org/issue8732
|
||||
|
||||
https://github.com/pypa/pip/blob/d0fa66ecc03ab20b7411b35f7c7b423f31f77761/pip/download.py#L147
|
||||
"""
|
||||
|
||||
return six.moves.urllib.request.Request(url, headers={'Accept-encoding': 'identity'})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def _get_opener(scheme=None):
|
||||
"""
|
||||
Build a urllib2 opener based on whether the user now wants SSL.
|
||||
|
||||
https://github.com/pypa/pip/blob/d0fa66ecc03ab20b7411b35f7c7b423f31f77761/pip/download.py#L178
|
||||
Raises tuf.exceptions.Error if tuf.settings.ssl_certificates is not a valid
|
||||
file.
|
||||
"""
|
||||
|
||||
if scheme == "https":
|
||||
if not os.path.isfile(tuf.settings.ssl_certificates):
|
||||
raise tuf.exceptions.Error('The SSL certificate specified in'
|
||||
' tuf.settings.ssl_certificates is not a valid file.'
|
||||
' ssl_certificates set to: ' + repr(tuf.settings.ssl_certificates))
|
||||
|
||||
else:
|
||||
# If we are going over https, use an opener which will provide SSL
|
||||
# certificate verification.
|
||||
https_handler = VerifiedHTTPSHandler()
|
||||
opener = six.moves.urllib.request.build_opener(https_handler)
|
||||
|
||||
# Strip out HTTPHandler to prevent MITM spoof.
|
||||
for handler in opener.handlers:
|
||||
if isinstance(handler, six.moves.urllib.request.HTTPHandler):
|
||||
opener.handlers.remove(handler)
|
||||
|
||||
else:
|
||||
# Otherwise, use the default opener.
|
||||
opener = six.moves.urllib.request.build_opener()
|
||||
|
||||
return opener
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def _open_connection(url):
|
||||
"""
|
||||
<Purpose>
|
||||
Helper function that opens a connection to the url. urllib2 supports http,
|
||||
ftp, and file. In python (2.6+) where the ssl module is available, urllib2
|
||||
also supports https.
|
||||
|
||||
TODO: Determine whether this follows http redirects and decide if we like
|
||||
that. For example, would we not want to allow redirection from ssl to
|
||||
non-ssl urls?
|
||||
|
||||
<Arguments>
|
||||
url:
|
||||
URL string (e.g., 'http://...' or 'ftp://...' or 'file://...')
|
||||
|
||||
<Exceptions>
|
||||
tuf.exceptions.Error, if the certificates pointed by
|
||||
tuf.settings.ssl_certificates cannot be loaded.
|
||||
|
||||
<Side Effects>
|
||||
Opens a connection to a remote server.
|
||||
|
||||
<Returns>
|
||||
File-like object.
|
||||
"""
|
||||
|
||||
# urllib2.Request produces a Request object that allows for a finer control
|
||||
# of the requesting process. Request object allows to add headers or data to
|
||||
# the HTTP request. For instance, request method add_header(key, val) can be
|
||||
# used to change/spoof 'User-Agent' from default Python-urllib/x.y to
|
||||
# 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)' this can be useful if
|
||||
# servers do not recognize connections that originates from
|
||||
# Python-urllib/x.y.
|
||||
|
||||
parsed_url = six.moves.urllib.parse.urlparse(url)
|
||||
opener = _get_opener(scheme=parsed_url.scheme)
|
||||
request = _get_request(url)
|
||||
|
||||
return opener.open(request, timeout = tuf.settings.SOCKET_TIMEOUT)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def _get_content_length(connection):
|
||||
def _get_content_length(response):
|
||||
"""
|
||||
<Purpose>
|
||||
A helper function that gets the purported file length from server.
|
||||
|
||||
<Arguments>
|
||||
connection:
|
||||
The object that the _open_connection function returns for communicating
|
||||
with the server about the contents of a URL.
|
||||
response:
|
||||
The object for communicating with the server about the contents of a URL.
|
||||
|
||||
<Side Effects>
|
||||
No known side effects.
|
||||
|
|
@ -525,7 +448,7 @@ def _get_content_length(connection):
|
|||
|
||||
try:
|
||||
# What is the length of this document according to the HTTP spec?
|
||||
reported_length = connection.info().get('Content-Length')
|
||||
reported_length = response.headers.get('Content-Length')
|
||||
|
||||
# Try casting it as a decimal number.
|
||||
reported_length = int(reported_length, 10)
|
||||
|
|
@ -536,7 +459,7 @@ def _get_content_length(connection):
|
|||
|
||||
except Exception as e:
|
||||
logger.exception('Could not get content length'
|
||||
' about ' + str(connection) + ' from server: ' + str(e))
|
||||
' about ' + str(response) + ' from server: ' + str(e))
|
||||
return None
|
||||
|
||||
return reported_length
|
||||
|
|
@ -682,71 +605,3 @@ def _check_downloaded_length(total_downloaded, required_length,
|
|||
|
||||
logger.info('Downloaded ' + str(total_downloaded) + ' bytes out of an'
|
||||
' upper limit of ' + str(required_length) + ' bytes.')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class VerifiedHTTPSConnection(six.moves.http_client.HTTPSConnection):
|
||||
"""
|
||||
A connection that wraps connections with ssl certificate verification.
|
||||
|
||||
https://github.com/pypa/pip/blob/d0fa66ecc03ab20b7411b35f7c7b423f31f77761/pip/download.py#L72
|
||||
Raise tuf.exeptions.Error if the certificates specified in
|
||||
tuf.settings.ssl_certificates cannot be loaded.
|
||||
"""
|
||||
|
||||
def connect(self):
|
||||
|
||||
connection_kwargs = {}
|
||||
connection_kwargs.update(timeout = self.timeout)
|
||||
|
||||
# for >= py2.7
|
||||
if hasattr(self, 'source_address'):
|
||||
connection_kwargs.update(source_address = self.source_address)
|
||||
|
||||
sock = socket.create_connection((self.host, self.port), **connection_kwargs)
|
||||
|
||||
# for >= py2.7
|
||||
if getattr(self, '_tunnel_host', None):
|
||||
self.sock = sock
|
||||
self._tunnel()
|
||||
|
||||
# set location of certificate authorities
|
||||
if not os.path.isfile(tuf.settings.ssl_certificates):
|
||||
raise tuf.exceptions.Error('The SSL certificate specified in'
|
||||
' tuf.settings.ssl_certificates is not a valid file.'
|
||||
' ssl_certificates set to: ' + repr(tuf.settings.ssl_certificates))
|
||||
|
||||
else:
|
||||
cert_path = tuf.settings.ssl_certificates
|
||||
|
||||
# TODO: Disallow SSLv2.
|
||||
# http://docs.python.org/dev/library/ssl.html#protocol-versions
|
||||
# TODO: Select the right ciphers.
|
||||
# http://docs.python.org/dev/library/ssl.html#cipher-selection
|
||||
# ssl.PROTOCOL_SSLv23, the default value for 'ssl_version', is deprecated in
|
||||
# Python 2.7.13, but becomes an alias for ssl.PROTOCOL_TLS.
|
||||
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file,
|
||||
cert_reqs=ssl.CERT_REQUIRED, ca_certs=cert_path,
|
||||
ssl_version=ssl.PROTOCOL_SSLv23)
|
||||
|
||||
match_hostname(self.sock.getpeercert(), self.host)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class VerifiedHTTPSHandler(six.moves.urllib.request.HTTPSHandler):
|
||||
"""
|
||||
A HTTPSHandler that uses our own VerifiedHTTPSConnection.
|
||||
|
||||
https://github.com/pypa/pip/blob/d0fa66ecc03ab20b7411b35f7c7b423f31f77761/pip/download.py#L109
|
||||
"""
|
||||
|
||||
def __init__(self, connection_class = VerifiedHTTPSConnection):
|
||||
self.specialized_conn_class = connection_class
|
||||
six.moves.urllib.request.HTTPSHandler.__init__(self)
|
||||
|
||||
def https_open(self, req):
|
||||
return self.do_open(self.specialized_conn_class, req)
|
||||
|
|
|
|||
|
|
@ -172,8 +172,8 @@ def __init__(self, expected_length, observed_length):
|
|||
self.observed_length = observed_length #bytes
|
||||
|
||||
def __str__(self):
|
||||
return 'Observed length (' + repr(self.observed_length)+\
|
||||
') <= expected length (' + repr(self.expected_length) + ').'
|
||||
return 'Observed length (' + repr(self.observed_length) + \
|
||||
') < expected length (' + repr(self.expected_length) + ').'
|
||||
|
||||
|
||||
class SlowRetrievalError(DownloadError):
|
||||
|
|
@ -270,6 +270,10 @@ class URLMatchesNoPatternError(Error):
|
|||
"""If a URL does not match a user-specified regular expression."""
|
||||
pass
|
||||
|
||||
class URLParsingError(Error):
|
||||
"""If we are unable to parse a URL -- for example, if a hostname element
|
||||
cannot be isoalted."""
|
||||
pass
|
||||
|
||||
class InvalidConfigurationError(Error):
|
||||
"""If a configuration object does not match the expected format."""
|
||||
|
|
|
|||
|
|
@ -46,11 +46,6 @@
|
|||
# /tmp/repositories/django_repo/metadata/current/root.METADATA_EXTENSION
|
||||
repositories_directory = None
|
||||
|
||||
# A PEM (RFC 1422) file where you may find SSL certificate authorities
|
||||
# https://en.wikipedia.org/wiki/Certificate_authority
|
||||
# http://docs.python.org/2/library/ssl.html#certificates
|
||||
ssl_certificates = None
|
||||
|
||||
# The 'log.py' module manages TUF's logging system. Users have the option to
|
||||
# enable/disable logging to a file via 'ENABLE_FILE_LOGGING', or
|
||||
# tuf.log.enable_file_logging() and tuf.log.disable_file_logging().
|
||||
|
|
@ -89,16 +84,6 @@
|
|||
# avoid being considered as a slow retrieval attack.
|
||||
MIN_AVERAGE_DOWNLOAD_SPEED = 50 #bytes/second
|
||||
|
||||
# The time (in seconds) we ignore a server with a slow initial retrieval speed.
|
||||
SLOW_START_GRACE_PERIOD = 0.1 #seconds
|
||||
|
||||
# Software updaters that integrate the framework are required to specify
|
||||
# the URL prefix for the mirrors that clients can contact to download updates.
|
||||
# The following URI schemes are those that download.py support. By default,
|
||||
# the ['http', 'https'] URI schemes are supported, but may be modified by
|
||||
# integrators to schemes that they wish to support for their integration.
|
||||
SUPPORTED_URI_SCHEMES = ['http', 'https']
|
||||
|
||||
# By default, limit number of delegatees we visit for any target.
|
||||
MAX_NUMBER_OF_DELEGATIONS = 2**5
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue