diff --git a/src/tuf/interposition.py b/src/tuf/interposition.py index 1d7b0087..f359696d 100644 --- a/src/tuf/interposition.py +++ b/src/tuf/interposition.py @@ -1,4 +1,6 @@ import httplib +import json +import logging import os.path import tempfile import tuf.client.updater @@ -8,6 +10,12 @@ import urlparse +# TODO: +# - failsafe: if TUF fails, offer option to unsafely resort back to urllib/urllib2 +# - match URLs not with hostnames, but with regular expressions +_logger = logging.getLogger( "tuf.interposition" ) + + class TUFConfiguration( object ): def __init__( self, hostname, repository_directory, repository_mirrors ): self.hostname = hostname @@ -15,6 +23,67 @@ def __init__( self, hostname, repository_directory, repository_mirrors ): self.repository_mirrors = repository_mirrors self.tempdir = tempfile.mkdtemp() + @staticmethod + def load_from_json( hostname, configuration ): + repository_directory = configuration[ "repository_directory" ] + repository_mirrors = configuration[ "repository_mirrors" ] + + return TUFConfiguration( + hostname, + repository_directory, + repository_mirrors + ) + + +class TUFUpdater( object ): + # hostname: str -> tuf_configuration: TUFConfiguration + __tuf_configurations = {} + + def __init__( self, url, parsed_url, tuf_configuration ): + self.url = url + self.parsed_url = parsed_url + self.tuf_configuration = tuf_configuration + + # must switch context before instantiating updater + # because updater depends on some module (tuf.conf) variables + self.switch_context() + self.updater = tuf.client.updater.Updater( + self.parsed_url.hostname, + self.tuf_configuration.repository_mirrors + ) + + @staticmethod + def add_tuf_configuration( tuf_configuration ): + assert isinstance( tuf_configuration, TUFConfiguration ) + assert tuf_configuration.hostname not in TUFUpdater.__tuf_configurations + + TUFUpdater.__tuf_configurations[ + tuf_configuration.hostname + ] = tuf_configuration + + @staticmethod + def make_tuf_updater( url ): + parsed_url = urlparse.urlparse( url ) + # TODO: enable specificity beyond hostname (e.g. include scheme, port) + tuf_configuration = \ + TUFUpdater.__tuf_configurations.get( parsed_url.hostname ) + + if tuf_configuration is None: + return None + else: + return TUFUpdater( url, parsed_url, tuf_configuration ) + + def make_tempfile( self, target_filepath ): + destination_directory = self.tuf_configuration.tempdir + filename = os.path.join( destination_directory, target_filepath ) + return destination_directory, filename + + # TODO: not thread-safe + def switch_context( self ): + # Set the local repository directory containing the metadata files. + tuf.conf.repository_directory = \ + self.tuf_configuration.repository_directory + # TODO: distinguish between urllib and urllib2 contracts class TUFDownloadMixin( object ): @@ -128,53 +197,66 @@ def http_open( self, req ): return response -class TUFUpdater( object ): - # hostname: str -> tuf_configuration: TUFConfiguration - _tuf_configurations = {} +def interpose( filename = "tuf.interposition.json" ): + INVALID_TUF_CONFIGURATION = "Invalid TUF configuration for " + \ + "{hostname}! TUF interposition will NOT be present for {hostname}." + INVALID_TUF_INTERPOSITION_JSON = "Invalid TUF configuration JSON file " + \ + "{filename}! TUF interposition will NOT be present for any host." + NO_HOSTNAMES = "No hostnames found in TUF configuration JSON file " + \ + "{filename}! TUF interposition will NOT be present for any host." - def __init__( self, url, parsed_url, tuf_configuration ): - self.url = url - self.parsed_url = parsed_url - self.tuf_configuration = tuf_configuration + """ + { + 'hostnames' : { + 'seattle.cs.washington.edu': { + 'repository_directory': '.client/', + 'repository_mirrors' : { + 'mirror1': { + 'url_prefix': 'http://seattle-tuf.cs.washington.edu', + 'metadata_path': 'metadata', + 'targets_path': 'targets', + 'confined_target_paths': [ '' ] + } + } + } + } + } + """ + try: + with open( filename ) as tuf_interposition_json: + tuf_interpositions = json.load( tuf_interposition_json ) + hostnames = tuf_interpositions.get( 'hostnames', {} ) - # must switch context before instantiating updater - # because updater depends on some module (tuf.conf) variables - self.switch_context() - self.updater = tuf.client.updater.Updater( - self.parsed_url.hostname, - self.tuf_configuration.repository_mirrors + # TODO: more input sanity checks + if len( hostnames ) == 0: + log_warning( NO_HOSTNAMES.format( filename = filename ) ) + else: + for hostname, configuration in hostnames.iteritems(): + try: + TUFUpdater.add_tuf_configuration( + TUFConfiguration.load_from_json( + hostname, + configuration + ) + ) + except: + log_warning( + INVALID_TUF_CONFIGURATION.format( + hostname = hostname + ) + ) + except: + log_warning( + INVALID_TUF_INTERPOSITION_JSON.format( filename = filename ) ) + else: + # http://docs.python.org/2/library/urllib.html#urllib._urlopener + urllib._urlopener = TUFancyURLOpener() - @staticmethod - def make_tuf_updater( url ): - parsed_url = urlparse.urlparse( url ) - # TODO: enable specificity beyond hostname (e.g. include scheme, port) - tuf_configuration = \ - TUFUpdater._tuf_configurations.get( parsed_url.hostname ) - - if tuf_configuration is None: - return None - else: - return TUFUpdater( url, parsed_url, tuf_configuration ) - - def make_tempfile( self, target_filepath ): - destination_directory = self.tuf_configuration.tempdir - filename = os.path.join( destination_directory, target_filepath ) - return destination_directory, filename - - # TODO: not thread-safe - def switch_context( self ): - # Set the local repository directory containing the metadata files. - tuf.conf.repository_directory = \ - self.tuf_configuration.repository_directory - - -# TODO: setup based on JSON file -def interpose( tuf_configuration ): - if isinstance( tuf_configuration, TUFConfiguration ): - TUFUpdater._tuf_configurations[ - tuf_configuration.hostname - ] = tuf_configuration + # http://docs.python.org/2/library/urllib2.html#urllib2.build_opener + # http://docs.python.org/2/library/urllib2.html#urllib2.install_opener + # TODO: override other default urllib2 handlers + urllib2.install_opener( urllib2.build_opener( TUFHTTPHandler ) ) def go_away(): @@ -182,10 +264,10 @@ def go_away(): raise NotImplementedError -# http://docs.python.org/2/library/urllib.html#urllib._urlopener -urllib._urlopener = TUFancyURLOpener() +def log_exception( message ): + _logger.exception( message ) -# http://docs.python.org/2/library/urllib2.html#urllib2.build_opener -# http://docs.python.org/2/library/urllib2.html#urllib2.install_opener -# TODO: override other default urllib2 handlers -urllib2.install_opener( urllib2.build_opener( TUFHTTPHandler ) ) + +def log_warning( message ): + _logger.warn( message ) + log_exception( message )