If received data isn't GPG signed then log the actual data at 'info' level.
[zeroinstall.git] / zeroinstall / injector / iface_cache.py
blobfba36e97f42685c246eb1c929882f98703e540c3
1 # Copyright (C) 2006, Thomas Leonard
2 # See the README file for details, or visit http://0install.net.
4 """The interface cache stores downloaded and verified interfaces in ~/.cache/0install.net/interfaces (by default).
5 There are methods to query the cache, add to it, check signatures, etc."""
7 import os, sys, time
8 from logging import debug, info, warn
9 from cStringIO import StringIO
11 from zeroinstall.injector import reader, basedir
12 from zeroinstall.injector.namespaces import *
13 from zeroinstall.injector.model import *
14 from zeroinstall import zerostore
16 def _pretty_time(t):
17 assert isinstance(t, (int, long))
18 return time.strftime('%Y-%m-%d %H:%M:%S UTC', time.localtime(t))
20 class IfaceCache(object):
21 __slots__ = ['watchers', '_interfaces', 'stores']
23 def __init__(self):
24 self.watchers = []
25 self._interfaces = {}
27 self.stores = zerostore.Stores()
29 def add_watcher(self, w):
30 assert w not in self.watchers
31 self.watchers.append(w)
33 def check_signed_data(self, interface, signed_data, handler):
34 """Downloaded data is a GPG-signed message. Check that the signature is trusted
35 and call self.update_interface_from_network() when done.
36 Calls handler.confirm_trust_keys() if keys are not trusted.
37 """
38 assert isinstance(interface, Interface)
39 import gpg
40 try:
41 data, sigs = gpg.check_stream(signed_data)
42 except:
43 signed_data.seek(0)
44 info("Failed to check GPG signature. Data received was:\n" + `signed_data.read()`)
45 raise
47 new_keys = False
48 import_error = None
49 for x in sigs:
50 need_key = x.need_key()
51 if need_key:
52 try:
53 self.download_key(interface, need_key)
54 except SafeException, ex:
55 import_error = ex
56 new_keys = True
58 if new_keys:
59 signed_data.seek(0)
60 data, sigs = gpg.check_stream(signed_data)
61 # If we got an error importing the keys, then report it now.
62 # If we still have missing keys, raise it as an exception, but
63 # if the keys got imported, just print and continue...
64 if import_error:
65 for x in sigs:
66 if x.need_key():
67 raise import_error
68 print >>sys.stderr, str(ex)
70 iface_xml = data.read()
71 data.close()
73 if not sigs:
74 raise SafeException('No signature on %s!\n'
75 'Possible reasons:\n'
76 '- You entered the interface URL incorrectly.\n'
77 '- The server delivered an error; try viewing the URL in a web browser.\n'
78 '- The developer gave you the URL of the unsigned interface by mistake.'
79 % interface.uri)
81 if not self.update_interface_if_trusted(interface, sigs, iface_xml):
82 handler.confirm_trust_keys(interface, sigs, iface_xml)
84 def update_interface_if_trusted(self, interface, sigs, xml):
85 for s in sigs:
86 if s.is_trusted():
87 self.update_interface_from_network(interface, xml, s.get_timestamp())
88 return True
89 return False
91 def download_key(self, interface, key_id):
92 assert interface
93 assert key_id
94 import urlparse, urllib2, shutil, tempfile
95 key_url = urlparse.urljoin(interface.uri, '%s.gpg' % key_id)
96 info("Fetching key from %s", key_url)
97 try:
98 stream = urllib2.urlopen(key_url)
99 # Python2.4: can't call fileno() on stream, so save to tmp file instead
100 tmpfile = tempfile.TemporaryFile(prefix = 'injector-dl-data-')
101 shutil.copyfileobj(stream, tmpfile)
102 tmpfile.flush()
103 stream.close()
104 except Exception, ex:
105 raise SafeException("Failed to download key from '%s': %s" % (key_url, str(ex)))
107 import gpg
109 tmpfile.seek(0)
110 gpg.import_key(tmpfile)
111 tmpfile.close()
113 def update_interface_from_network(self, interface, new_xml, modified_time):
114 """xml is the new XML (after the signature has been checked and
115 removed). modified_time will be set as an attribute on the root."""
116 debug("Updating '%s' from network; modified at %s" %
117 (interface.name or interface.uri, _pretty_time(modified_time)))
119 from xml.dom import minidom
120 doc = minidom.parseString(new_xml)
121 doc.documentElement.setAttribute('last-modified', str(modified_time))
122 new_xml = StringIO()
123 doc.writexml(new_xml)
125 self.import_new_interface(interface, new_xml.getvalue())
127 import writer
128 interface.last_checked = long(time.time())
129 writer.save_interface(interface)
131 info("Updated interface cache entry for %s (modified %s)",
132 interface.get_name(), _pretty_time(modified_time))
134 for w in self.watchers:
135 w.interface_changed(interface)
137 def import_new_interface(self, interface, new_xml):
138 upstream_dir = basedir.save_cache_path(config_site, 'interfaces')
139 cached = os.path.join(upstream_dir, escape(interface.uri))
141 if os.path.exists(cached):
142 old_xml = file(cached).read()
143 if old_xml == new_xml:
144 debug("No change")
145 return
147 stream = file(cached + '.new', 'w')
148 stream.write(new_xml)
149 stream.close()
150 new_mtime = reader.check_readable(interface.uri, cached + '.new')
151 assert new_mtime
152 if interface.last_modified:
153 if new_mtime < interface.last_modified:
154 raise SafeException("New interface's modification time is before old "
155 "version!"
156 "\nOld time: " + _pretty_time(interface.last_modified) +
157 "\nNew time: " + _pretty_time(new_mtime) +
158 "\nRefusing update (leaving new copy as " +
159 cached + ".new)")
160 if new_mtime == interface.last_modified:
161 raise SafeException("Interface has changed, but modification time "
162 "hasn't! Refusing update.")
163 os.rename(cached + '.new', cached)
164 debug("Saved as " + cached)
166 reader.update_from_cache(interface)
168 def get_interface(self, uri):
169 """Get the interface for uri, creating a new one if required.
170 New interfaces are initialised from the disk cache, but not from
171 the network."""
172 if type(uri) == str:
173 uri = unicode(uri)
174 assert isinstance(uri, unicode)
176 if uri in self._interfaces:
177 return self._interfaces[uri]
179 debug("Initialising new interface object for %s", uri)
180 self._interfaces[uri] = Interface(uri)
181 reader.update_from_cache(self._interfaces[uri])
182 return self._interfaces[uri]
184 def list_all_interfaces(self):
185 all = {}
186 for d in basedir.load_cache_paths(config_site, 'interfaces'):
187 for leaf in os.listdir(d):
188 if not leaf.startswith('.'):
189 all[leaf] = True
190 for d in basedir.load_config_paths(config_site, config_prog, 'user_overrides'):
191 for leaf in os.listdir(d):
192 if not leaf.startswith('.'):
193 all[leaf] = True
194 return map(unescape, all.keys())
196 def add_to_cache(self, source, data):
197 assert isinstance(source, DownloadSource)
198 required_digest = source.implementation.id
199 url = source.url
200 self.stores.add_archive_to_cache(required_digest, data, source.url, source.extract,
201 type = source.type, start_offset = source.start_offset or 0)
203 def get_icon_path(self, iface):
204 "Get the path of the cached icon for this interface, or None if not cached."
205 return basedir.load_first_cache(config_site, 'interface_icons',
206 escape(iface.uri))
208 iface_cache = IfaceCache()