Logo Search packages:      
Sourcecode: zeroinstall-injector version File versions

iface_cache.py

00001 """
Manages the interface cache.

@var iface_cache: A singleton cache object. You should normally use this rather than
creating new cache objects.
"""
# Copyright (C) 2006, Thomas Leonard
# See the README file for details, or visit http://0install.net.

# Note:
#
# We need to know the modification time of each interface, because we refuse
# to update to an older version (this prevents an attack where the attacker
# sends back an old version which is correctly signed but has a known bug).
#
# The way we store this is a bit complicated due to backward compatibility:
#
# - GPG-signed interfaces have their signatures removed and a last-modified
#   attribute is stored containing the date from the signature.
#
# - XML-signed interfaces are stored unmodified with their signatures. The
#   date is extracted from the signature when needed.
#
# - Older versions used to add the last-modified attribute even to files
#   with XML signatures - these files therefore have invalid signatures and
#   we extract from the attribute for these.
#
# Eventually, support for the first and third cases will be removed.

import os, sys, time
from logging import debug, info, warn
from cStringIO import StringIO

from zeroinstall.injector import reader, basedir
from zeroinstall.injector.namespaces import *
from zeroinstall.injector.model import *
from zeroinstall import zerostore

def _pretty_time(t):
      assert isinstance(t, (int, long))
      return time.strftime('%Y-%m-%d %H:%M:%S UTC', time.localtime(t))

00043 class PendingFeed(object):
      """A feed that has been downloaded but not yet added to the interface cache.
      Feeds remain in this state until the user confirms that they trust at least
      one of the signatures.
      @ivar url: URL for the feed
      @type url: str
      @ivar signed_data: the untrusted data
      @type signed_data: stream
      @ivar sigs: signatures extracted from signed_data
      @type sigs: [L{gpg.Signature}]
      @ivar new_xml: the payload of the signed_data, or the whole thing if XML
      @type new_xml: str
      @since: 0.25"""
      __slots__ = ['url', 'signed_data', 'sigs', 'new_xml', 'downloads', 'download_callback']

00058       def __init__(self, url, signed_data):
            """Downloaded data is a GPG-signed message.
            @param url: the URL of the downloaded feed
            @type url: str
            @param signed_data: the downloaded data (not yet trusted)
            @type signed_data: stream
            @raise SafeException: if the data is not signed, and logs the actual data"""
            self.url = url
            self.signed_data = signed_data
            self.downloads = []
            self.recheck()

00070       def begin_key_downloads(self, handler, callback):
            """Start downloading any required GPG keys not already on our keyring.
            When all downloads are done (successful or otherwise), add any new keys
            to the keyring, L{recheck}, and invoke the callback.
            If we are already downloading, return and do nothing else.
            Otherwise, if nothing needs to be downloaded, the callback is invoked immediately.
            @param handler: handler to manage the download
            @type handler: L{handler.Handler}
            @param callback: callback to invoke when done
            @type callback: function()
            """
            if self.downloads:
                  return

            assert callback
            self.download_callback = callback

            for x in self.sigs:
                  key_id = x.need_key()
                  if key_id:
                        import urlparse
                        key_url = urlparse.urljoin(self.url, '%s.gpg' % key_id)
                        info("Fetching key from %s", key_url)
                        dl = handler.get_download(key_url)
                        self.downloads.append(dl)
                        dl.on_success.append(lambda stream: self._downloaded_key(dl, stream))

            if not self.downloads:
                  self.download_callback()

      def _downloaded_key(self, dl, stream):
            import shutil, tempfile
            from zeroinstall.injector import gpg

            self.downloads.remove(dl)

            info("Importing key for feed '%s'", self.url)

            # Python2.4: can't call fileno() on stream, so save to tmp file instead
            tmpfile = tempfile.TemporaryFile(prefix = 'injector-dl-data-')
            try:
                  shutil.copyfileobj(stream, tmpfile)
                  tmpfile.flush()

                  try:
                        tmpfile.seek(0)
                        gpg.import_key(tmpfile)
                  except Exception, ex:
                        warn("Failed to import key for '%s': %s", self.url, str(ex))
            finally:
                  tmpfile.close()

                  if not self.downloads:
                        # All complete
                        self.recheck()
                        self.download_callback()

00127       def recheck(self):
            """Set new_xml and sigs by reading signed_data.
            You need to call this when previously-missing keys are added to the GPG keyring."""
            import gpg
            try:
                  self.signed_data.seek(0)
                  stream, sigs = gpg.check_stream(self.signed_data)
                  assert sigs

                  data = stream.read()
                  if stream is not self.signed_data:
                        stream.close()

                  self.new_xml = data
                  self.sigs = sigs
            except:
                  self.signed_data.seek(0)
                  info("Failed to check GPG signature. Data received was:\n" + `self.signed_data.read()`)
                  raise

00147 class IfaceCache(object):
      """
      The interface cache stores downloaded and verified interfaces in
      ~/.cache/0install.net/interfaces (by default).

      There are methods to query the cache, add to it, check signatures, etc.

      When updating the cache, the normal sequence is as follows:

       1. When the data arrives, L{add_pending} is called.
       2. Later (typically during a recalculate), L{policy.Policy.get_interface}
          notices the pending feed and starts processing it.
       3. It checks the signatures using L{PendingFeed.sigs}.
       4. If any required GPG keys are missing, L{download_key} is used to fetch
          them first.
       5. If none of the keys are trusted, L{handler.Handler.confirm_trust_keys} is called.
       6. L{update_interface_if_trusted} is called to update the cache.

      Whenever something needs to be done before the feed can move from the pending
      state, the process is resumed after the required activity by calling L{policy.Policy.recalculate}.

      @ivar watchers: objects requiring notification of cache changes.
      @ivar pending: downloaded feeds which are not yet trusted
      @type pending: str -> PendingFeed
      @see: L{iface_cache} - the singleton IfaceCache instance.
      """

      __slots__ = ['watchers', '_interfaces', 'stores', 'pending']

      def __init__(self):
            self.watchers = []
            self._interfaces = {}
            self.pending = {}

            self.stores = zerostore.Stores()
      
00183       def add_watcher(self, w):
            """Call C{w.interface_changed(iface)} each time L{update_interface_from_network}
            changes an interface in the cache."""
            assert w not in self.watchers
            self.watchers.append(w)
      
00189       def add_pending(self, pending):
            """Add a PendingFeed to the pending dict.
            @param pending: the untrusted download to add
            @type pending: PendingFeed
            @since: 0.25"""
            assert isinstance(pending, PendingFeed)
            self.pending[pending.url] = pending
      
00197       def update_interface_if_trusted(self, interface, sigs, xml):
            """Update a cached interface (using L{update_interface_from_network})
            if we trust the signatures, and remove it from L{pending}.
            If we don't trust any of the signatures, do nothing.
            @param interface: the interface being updated
            @type interface: L{model.Interface}
            @param sigs: signatures from L{gpg.check_stream}
            @type sigs: [L{gpg.Signature}]
            @param xml: the downloaded replacement interface document
            @type xml: str
            @return: True if the interface was updated
            @rtype: bool
            @precondition: call L{add_pending}
            """
            updated = self._oldest_trusted(sigs)
            if updated is None: return False    # None are trusted
      
            if interface.uri in self.pending:
                  del self.pending[interface.uri]
            else:
                  raise Exception("update_interface_if_trusted, but '%s' not pending!" % interface.uri)

            self.update_interface_from_network(interface, xml, updated)
            return True

00222       def download_key(self, interface, key_id):
            """Download a GPG key.
            The location of the key is calculated from the uri of the interface.
            @param interface: the interface which needs the key
            @param key_id: the GPG long id of the key
            @todo: This method blocks. It should start a download and return.
            @deprecated: see PendingFeed
            """
            assert interface
            assert key_id
            import urlparse, urllib2, shutil, tempfile
            key_url = urlparse.urljoin(interface.uri, '%s.gpg' % key_id)
            info("Fetching key from %s", key_url)
            try:
                  stream = urllib2.urlopen(key_url)
                  # Python2.4: can't call fileno() on stream, so save to tmp file instead
                  tmpfile = tempfile.TemporaryFile(prefix = 'injector-dl-data-')
                  shutil.copyfileobj(stream, tmpfile)
                  tmpfile.flush()
                  stream.close()
            except Exception, ex:
                  raise SafeException("Failed to download key from '%s': %s" % (key_url, str(ex)))

            import gpg

            tmpfile.seek(0)
            gpg.import_key(tmpfile)
            tmpfile.close()

00251       def update_interface_from_network(self, interface, new_xml, modified_time):
            """Update a cached interface.
            Called by L{update_interface_if_trusted} if we trust this data.
            After a successful update, L{writer} is used to update the interface's
            last_checked time and then all the L{watchers} are notified.
            @param interface: the interface being updated
            @type interface: L{model.Interface}
            @param new_xml: the downloaded replacement interface document
            @type new_xml: str
            @param modified_time: the timestamp of the oldest trusted signature
            (used as an approximation to the interface's modification time)
            @type modified_time: long
            @raises SafeException: if modified_time is older than the currently cached time
            """
            debug("Updating '%s' from network; modified at %s" %
                  (interface.name or interface.uri, _pretty_time(modified_time)))

            if '\n<!-- Base64 Signature' not in new_xml:
                  # Only do this for old-style interfaces without
                  # signatures Otherwise, we can get the time from the
                  # signature, and adding this attribute just makes the
                  # signature invalid.
                  from xml.dom import minidom
                  doc = minidom.parseString(new_xml)
                  doc.documentElement.setAttribute('last-modified', str(modified_time))
                  new_xml = StringIO()
                  doc.writexml(new_xml)
                  new_xml = new_xml.getvalue()

            self._import_new_interface(interface, new_xml, modified_time)

            import writer
            interface.last_checked = long(time.time())
            writer.save_interface(interface)

            info("Updated interface cache entry for %s (modified %s)",
                  interface.get_name(), _pretty_time(modified_time))

            for w in self.watchers:
                  w.interface_changed(interface)
      
00292       def _import_new_interface(self, interface, new_xml, modified_time):
            """Write new_xml into the cache.
            @param interface: updated once the new XML is written
            @param new_xml: the data to write
            @param modified_time: when new_xml was modified
            @raises SafeException: if the new mtime is older than the current one
            """
            assert modified_time

            upstream_dir = basedir.save_cache_path(config_site, 'interfaces')
            cached = os.path.join(upstream_dir, escape(interface.uri))

            if os.path.exists(cached):
                  old_xml = file(cached).read()
                  if old_xml == new_xml:
                        debug("No change")
                        return

            stream = file(cached + '.new', 'w')
            stream.write(new_xml)
            stream.close()
            os.utime(cached + '.new', (modified_time, modified_time))
            new_mtime = reader.check_readable(interface.uri, cached + '.new')
            assert new_mtime == modified_time

            old_modified = self._get_signature_date(interface.uri)
            if old_modified is None:
                  old_modified = interface.last_modified

            if old_modified:
                  if new_mtime < old_modified:
                        raise SafeException("New interface's modification time is before old "
                                        "version!"
                                        "\nOld time: " + _pretty_time(old_modified) +
                                        "\nNew time: " + _pretty_time(new_mtime) + 
                                        "\nRefusing update (leaving new copy as " +
                                        cached + ".new)")
                  if new_mtime == old_modified:
                        # You used to have to update the modification time manually.
                        # Now it comes from the signature, this check isn't useful
                        # and often causes problems when the stored format changes
                        # (e.g., when we stopped writing last-modified attributes)
                        pass
                        #raise SafeException("Interface has changed, but modification time "
                        #               "hasn't! Refusing update.")
            os.rename(cached + '.new', cached)
            debug("Saved as " + cached)

            reader.update_from_cache(interface)

00342       def get_interface(self, uri):
            """Get the interface for uri, creating a new one if required.
            New interfaces are initialised from the disk cache, but not from
            the network.
            @param uri: the URI of the interface to find
            @rtype: L{model.Interface}
            """
            if type(uri) == str:
                  uri = unicode(uri)
            assert isinstance(uri, unicode)

            if uri in self._interfaces:
                  return self._interfaces[uri]

            debug("Initialising new interface object for %s", uri)
            self._interfaces[uri] = Interface(uri)
            reader.update_from_cache(self._interfaces[uri])
            return self._interfaces[uri]

00361       def list_all_interfaces(self):
            """List all interfaces in the cache.
            @rtype: [str]
            """
            all = {}
            for d in basedir.load_cache_paths(config_site, 'interfaces'):
                  for leaf in os.listdir(d):
                        if not leaf.startswith('.'):
                              all[leaf] = True
            for d in basedir.load_config_paths(config_site, config_prog, 'user_overrides'):
                  for leaf in os.listdir(d):
                        if not leaf.startswith('.'):
                              all[leaf] = True
            return map(unescape, all.keys())

00376       def add_to_cache(self, source, data):
            """Add an implementation to the cache.
            @param source: information about the archive
            @type source: L{model.DownloadSource}
            @param data: the data stream
            @type data: stream
            @see: L{zerostore.Stores.add_archive_to_cache}
            """
            assert isinstance(source, DownloadSource)
            required_digest = source.implementation.id
            url = source.url
            self.stores.add_archive_to_cache(required_digest, data, source.url, source.extract,
                                     type = source.type, start_offset = source.start_offset or 0)
      
00390       def get_icon_path(self, iface):
            """Get the path of a cached icon for an interface.
            @param iface: interface whose icon we want
            @return: the path of the cached icon, or None if not cached.
            @rtype: str"""
            return basedir.load_first_cache(config_site, 'interface_icons',
                                     escape(iface.uri))

00398       def get_cached_signatures(self, uri):
            """Verify the cached interface using GPG.
            Only new-style XML-signed interfaces retain their signatures in the cache.
            @param uri: the feed to check
            @type uri: str
            @return: a list of signatures, or None
            @rtype: [L{gpg.Signature}] or None
            @since: 0.25"""
            import gpg
            if uri.startswith('/'):
                  old_iface = uri
            else:
                  old_iface = basedir.load_first_cache(config_site, 'interfaces', escape(uri))
                  if old_iface is None:
                        return None
            try:
                  return gpg.check_stream(file(old_iface))[1]
            except SafeException, ex:
                  debug("No signatures (old-style interface): %s" % ex)
                  return None
      
00419       def _get_signature_date(self, uri):
            """Read the date-stamp from the signature of the cached interface.
            If the date-stamp is unavailable, returns None."""
            sigs = self.get_cached_signatures(uri)
            if sigs:
                  return self._oldest_trusted(sigs)
      
00426       def _oldest_trusted(self, sigs):
            """Return the date of the oldest trusted signature in the list, or None if there
            are no trusted sigs in the list."""
            trusted = [s.get_timestamp() for s in sigs if s.is_trusted()]
            if trusted:
                  return min(trusted)
            return None

iface_cache = IfaceCache()

Generated by  Doxygen 1.6.0   Back to index