Connect to a key-server to lookup trust hints
[zeroinstall/zeroinstall-rsl.git] / zeroinstall / injector / handler.py
blobc95d4a47daf018285f5823878f3242be13d658a5
1 """
2 Integrates download callbacks with an external mainloop.
3 While things are being downloaded, Zero Install returns control to your program.
4 Your mainloop is responsible for monitoring the state of the downloads and notifying
5 Zero Install when they are complete.
7 To do this, you supply a L{Handler} to the L{policy}.
8 """
10 # Copyright (C) 2009, Thomas Leonard
11 # See the README file for details, or visit http://0install.net.
13 from zeroinstall import _
14 import sys
15 from logging import debug, warn
17 from zeroinstall import NeedDownload, SafeException
18 from zeroinstall.support import tasks
19 from zeroinstall.injector import download
21 class NoTrustedKeys(SafeException):
22 """Thrown by L{Handler.confirm_trust_keys} on failure."""
23 pass
25 class Handler(object):
26 """
27 This implementation uses the GLib mainloop. Note that QT4 can use the GLib mainloop too.
29 @ivar monitored_downloads: dict of downloads in progress
30 @type monitored_downloads: {URL: L{download.Download}}
31 @ivar n_completed_downloads: number of downloads which have finished for GUIs, etc (can be reset as desired).
32 @type n_completed_downloads: int
33 @ivar total_bytes_downloaded: informational counter for GUIs, etc (can be reset as desired). Updated when download finishes.
34 @type total_bytes_downloaded: int
35 """
37 __slots__ = ['monitored_downloads', '_loop', 'dry_run', 'total_bytes_downloaded', 'n_completed_downloads']
39 def __init__(self, mainloop = None, dry_run = False):
40 self.monitored_downloads = {}
41 self._loop = None
42 self.dry_run = dry_run
43 self.n_completed_downloads = 0
44 self.total_bytes_downloaded = 0
46 def monitor_download(self, dl):
47 """Called when a new L{download} is started.
48 This is mainly used by the GUI to display the progress bar."""
49 dl.start()
50 self.monitored_downloads[dl.url] = dl
51 self.downloads_changed()
53 @tasks.async
54 def download_done_stats():
55 yield dl.downloaded
56 # NB: we don't check for exceptions here; someone else should be doing that
57 try:
58 self.n_completed_downloads += 1
59 self.total_bytes_downloaded += dl.get_bytes_downloaded_so_far()
60 del self.monitored_downloads[dl.url]
61 self.downloads_changed()
62 except Exception, ex:
63 self.report_error(ex)
64 download_done_stats()
66 def impl_added_to_store(self, impl):
67 """Called by the L{fetch.Fetcher} when adding an implementation.
68 The GUI uses this to update its display.
69 @param impl: the implementation which has been added
70 @type impl: L{model.Implementation}
71 """
72 pass
74 def downloads_changed(self):
75 """This is just for the GUI to override to update its display."""
76 pass
78 def wait_for_blocker(self, blocker):
79 """Run a recursive mainloop until blocker is triggered.
80 @param blocker: event to wait on
81 @type blocker: L{tasks.Blocker}"""
82 if not blocker.happened:
83 import gobject
85 def quitter():
86 yield blocker
87 self._loop.quit()
88 quit = tasks.Task(quitter(), "quitter")
90 assert self._loop is None # Avoid recursion
91 self._loop = gobject.MainLoop(gobject.main_context_default())
92 try:
93 debug(_("Entering mainloop, waiting for %s"), blocker)
94 self._loop.run()
95 finally:
96 self._loop = None
98 assert blocker.happened, "Someone quit the main loop!"
100 tasks.check(blocker)
102 def get_download(self, url, force = False, hint = None):
103 """Return the Download object currently downloading 'url'.
104 If no download for this URL has been started, start one now (and
105 start monitoring it).
106 If the download failed and force is False, return it anyway.
107 If force is True, abort any current or failed download and start
108 a new one.
109 @rtype: L{download.Download}
111 if self.dry_run:
112 raise NeedDownload(url)
114 try:
115 dl = self.monitored_downloads[url]
116 if dl and force:
117 dl.abort()
118 raise KeyError
119 except KeyError:
120 dl = download.Download(url, hint)
121 self.monitor_download(dl)
122 return dl
124 def confirm_keys(self, pending, fetch_key_info):
125 """We don't trust any of the signatures yet. Ask the user.
126 When done update the L{trust} database, and then call L{trust.TrustDB.notify}.
127 This method just calls L{confirm_import_feed} if the handler (self) is
128 new-style, or L{confirm_trust_keys} for older classes. A class
129 is considered old-style if it overrides confirm_trust_keys and
130 not confirm_import_feed.
131 @since: 0.42
132 @arg pending: an object holding details of the updated feed
133 @type pending: L{PendingFeed}
134 @arg fetch_key_info: a function which can be used to fetch information about a key fingerprint
135 @type fetch_key_info: str -> L{Blocker}
136 @return: A blocker that triggers when the user has chosen, or None if already done.
137 @rtype: None | L{Blocker}"""
139 assert pending.sigs
141 if hasattr(self.confirm_trust_keys, 'original') or not hasattr(self.confirm_import_feed, 'original'):
142 # new-style class
143 return self.confirm_import_feed(pending, fetch_key_info)
144 else:
145 # old-style class
146 from zeroinstall.injector import iface_cache
147 import warnings
148 warnings.warn(_("Should override confirm_import_feed(); using old confirm_trust_keys() for now"), DeprecationWarning, stacklevel = 2)
150 iface = iface_cache.iface_cache.get_interface(pending.url)
151 return self.confirm_trust_keys(iface, pending.sigs, pending.new_xml)
153 @tasks.async
154 def confirm_import_feed(self, pending, fetch_key_info):
155 """Sub-classes should override this method to interact with the user about new feeds.
156 @since: 0.42
157 @see: L{confirm_keys}"""
158 from zeroinstall.injector import trust, gpg
159 valid_sigs = [s for s in pending.sigs if isinstance(s, gpg.ValidSig)]
160 if not valid_sigs:
161 raise SafeException('No valid signatures found on "%s". Signatures:%s' %
162 (pending.url, ''.join(['\n- ' + str(s) for s in pending.sigs])))
164 domain = trust.domain_from_url(pending.url)
166 # Ask on stderr, because we may be writing XML to stdout
167 print >>sys.stderr, "\nFeed:", pending.url
168 print >>sys.stderr, "The feed is correctly signed with the following keys:"
169 for x in valid_sigs:
170 print >>sys.stderr, "-", x
172 def text(parent):
173 text = ""
174 for node in parent.childNodes:
175 if node.nodeType == node.TEXT_NODE:
176 text = text + node.data
177 return text
179 kfs = [fetch_key_info(sig.fingerprint) for sig in valid_sigs]
180 while kfs:
181 old_kfs = kfs
182 kfs = []
183 for kf in old_kfs:
184 infos = kf.collect_info()
185 if infos:
186 if len(valid_sigs) > 1:
187 print "%s: " % kf.fingerprint
188 for info in infos:
189 print >>sys.stderr, "-", text(info)
190 if kf.blocker:
191 kfs.append(kf)
192 if kfs:
193 for kf in kfs: print >>sys.stderr, kf.status
194 blockers = [kf.blocker for kf in kfs]
195 yield blockers
196 for b in blockers:
197 try:
198 tasks.check(b)
199 except Exception, ex:
200 warn("Failed to get key info: %s", ex)
202 if len(valid_sigs) == 1:
203 print >>sys.stderr, "Do you want to trust this key to sign feeds from '%s'?" % domain
204 else:
205 print >>sys.stderr, "Do you want to trust all of these keys to sign feeds from '%s'?" % domain
206 while True:
207 print >>sys.stderr, "Trust [Y/N] ",
208 i = raw_input()
209 if not i: continue
210 if i in 'Nn':
211 raise NoTrustedKeys(_('Not signed with a trusted key'))
212 if i in 'Yy':
213 break
214 for key in valid_sigs:
215 print >>sys.stderr, "Trusting", key.fingerprint, "for", domain
216 trust.trust_db.trust_key(key.fingerprint, domain)
218 confirm_import_feed.original = True
220 def confirm_trust_keys(self, interface, sigs, iface_xml):
221 """We don't trust any of the signatures yet. Ask the user.
222 When done update the L{trust} database, and then call L{trust.TrustDB.notify}.
223 @deprecated: see L{confirm_keys}
224 @arg interface: the interface being updated
225 @arg sigs: a list of signatures (from L{gpg.check_stream})
226 @arg iface_xml: the downloaded data (not yet trusted)
227 @return: a blocker, if confirmation will happen asynchronously, or None
228 @rtype: L{tasks.Blocker}"""
229 import warnings
230 warnings.warn(_("Use confirm_keys, not confirm_trust_keys"), DeprecationWarning, stacklevel = 2)
231 from zeroinstall.injector import trust, gpg
232 assert sigs
233 valid_sigs = [s for s in sigs if isinstance(s, gpg.ValidSig)]
234 if not valid_sigs:
235 raise SafeException('No valid signatures found on "%s". Signatures:%s' %
236 (interface.uri, ''.join(['\n- ' + str(s) for s in sigs])))
238 domain = trust.domain_from_url(interface.uri)
240 # Ask on stderr, because we may be writing XML to stdout
241 print >>sys.stderr, "\nInterface:", interface.uri
242 print >>sys.stderr, "The interface is correctly signed with the following keys:"
243 for x in valid_sigs:
244 print >>sys.stderr, "-", x
246 if len(valid_sigs) == 1:
247 print >>sys.stderr, "Do you want to trust this key to sign feeds from '%s'?" % domain
248 else:
249 print >>sys.stderr, "Do you want to trust all of these keys to sign feeds from '%s'?" % domain
250 while True:
251 print >>sys.stderr, "Trust [Y/N] ",
252 i = raw_input()
253 if not i: continue
254 if i in 'Nn':
255 raise NoTrustedKeys(_('Not signed with a trusted key'))
256 if i in 'Yy':
257 break
258 for key in valid_sigs:
259 print >>sys.stderr, "Trusting", key.fingerprint, "for", domain
260 trust.trust_db.trust_key(key.fingerprint, domain)
262 trust.trust_db.notify()
264 confirm_trust_keys.original = True # Detect if someone overrides it
266 def report_error(self, exception, tb = None):
267 """Report an exception to the user.
268 @param exception: the exception to report
269 @type exception: L{SafeException}
270 @param tb: optional traceback
271 @since: 0.25"""
272 warn("%s", str(exception) or type(exception))
273 #import traceback
274 #traceback.print_exception(exception, None, tb)