]> git.lizzy.rs Git - torbrowser-launcher.git/blob - torbrowser_launcher/launcher.py
hides TBL window before launching TBB (#151)
[torbrowser-launcher.git] / torbrowser_launcher / launcher.py
1 """
2 Tor Browser Launcher
3 https://github.com/micahflee/torbrowser-launcher/
4
5 Copyright (c) 2013-2014 Micah Lee <micah@micahflee.com>
6
7 Permission is hereby granted, free of charge, to any person
8 obtaining a copy of this software and associated documentation
9 files (the "Software"), to deal in the Software without
10 restriction, including without limitation the rights to use,
11 copy, modify, merge, publish, distribute, sublicense, and/or sell
12 copies of the Software, and to permit persons to whom the
13 Software is furnished to do so, subject to the following
14 conditions:
15
16 The above copyright notice and this permission notice shall be
17 included in all copies or substantial portions of the Software.
18
19 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
21 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
23 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
24 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26 OTHER DEALINGS IN THE SOFTWARE.
27 """
28
29 import os, subprocess, time, json, tarfile, hashlib, lzma, threading
30 from twisted.internet import reactor
31 from twisted.web.client import Agent, RedirectAgent, ResponseDone, ResponseFailed
32 from twisted.web.http_headers import Headers
33 from twisted.internet.protocol import Protocol
34 from twisted.internet.ssl import ClientContextFactory
35 from twisted.internet.error import DNSLookupError
36
37 import OpenSSL
38
39 import pygtk
40 pygtk.require('2.0')
41 import gtk
42
43 class TryStableException(Exception):
44     pass
45
46 class TryDefaultMirrorException(Exception):
47     pass
48
49 class DownloadErrorException(Exception):
50     pass
51
52 class VerifyTorProjectCert(ClientContextFactory):
53     def __init__(self, torproject_pem):
54         self.torproject_ca = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, open(torproject_pem, 'r').read())
55
56     def getContext(self, host, port):
57         ctx = ClientContextFactory.getContext(self)
58         ctx.set_verify_depth(0)
59         ctx.set_verify(OpenSSL.SSL.VERIFY_PEER | OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname)
60         return ctx
61
62     def verifyHostname(self, connection, cert, errno, depth, preverifyOK):
63         return cert.digest('sha256') == self.torproject_ca.digest('sha256')
64
65 class Launcher:
66     def __init__(self, common):
67         print _('Starting launcher dialog')
68         self.common = common
69
70         # init launcher
71         self.set_gui(None, '', [])
72         self.launch_gui = True
73         print "LATEST VERSION", self.common.settings['latest_version']
74         self.common.build_paths(self.common.settings['latest_version'])
75
76         if self.common.settings['update_over_tor']:
77             try:
78                 import txsocksx
79             except ImportError:
80                 md = gtk.MessageDialog(None, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, _("The python-txsocksx package is missing, downloads will not happen over tor"))
81                 md.set_position(gtk.WIN_POS_CENTER)
82                 md.run()
83                 md.destroy()
84                 self.common.settings['update_over_tor'] = False
85                 self.common.save_settings()
86
87         # is firefox already running?
88         if self.common.settings['installed_version']:
89             firefox_pid = self.common.get_pid('./Browser/firefox')
90             if firefox_pid:
91                 print _('Firefox is open, bringing to focus')
92                 # bring firefox to front
93                 self.common.bring_window_to_front(firefox_pid)
94                 return
95
96         # check for updates?
97         check_for_updates = False
98         if self.common.settings['check_for_updates']:
99             check_for_updates = True
100
101         if not check_for_updates:
102             # how long was it since the last update check?
103             # 86400 seconds = 24 hours
104             current_timestamp = int(time.time())
105             if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
106                 check_for_updates = True
107
108         if check_for_updates:
109             # check for update
110             print 'Checking for update'
111             self.set_gui('task', _("Checking for Tor Browser update."),
112                          ['download_update_check',
113                           'attempt_update'])
114         else:
115             # no need to check for update
116             print _('Checked for update within 24 hours, skipping')
117             self.start_launcher()
118
119         if self.launch_gui:
120             # set up the window
121             self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
122             self.window.set_title(_("Tor Browser"))
123             self.window.set_icon_from_file(self.common.paths['icon_file'])
124             self.window.set_position(gtk.WIN_POS_CENTER)
125             self.window.set_border_width(10)
126             self.window.connect("delete_event", self.delete_event)
127             self.window.connect("destroy", self.destroy)
128
129             # build the rest of the UI
130             self.build_ui()
131
132     # download or run TBB
133     def start_launcher(self):
134         # is TBB already installed?
135         latest_version = self.common.settings['latest_version']
136         installed_version = self.common.settings['installed_version']
137
138         # verify installed version for newer versions of TBB (#58)
139         if installed_version >= '3.0':
140             versions_filename = self.common.paths['tbb']['versions']
141             if os.path.exists(versions_filename):
142                 for line in open(versions_filename):
143                     if 'TORBROWSER_VERSION' in line:
144                         installed_version = line.lstrip('TORBROWSER_VERSION=').strip()
145
146         start = self.common.paths['tbb']['start']
147         if os.path.isfile(start) and os.access(start, os.X_OK):
148             if installed_version == latest_version:
149                 print _('Latest version of TBB is installed, launching')
150                 # current version of tbb is installed, launch it
151                 self.run(False)
152                 self.launch_gui = False
153             elif installed_version < latest_version:
154                 print _('TBB is out of date, attempting to upgrade to {0}'.format(latest_version))
155                 # there is a tbb upgrade available
156                 self.set_gui('task', _("Your Tor Browser is out of date. Upgrading from {0} to {1}.".format(installed_version, latest_version)),
157                              ['download_sha256',
158                               'download_sha256_sig',
159                               'download_tarball',
160                               'verify',
161                               'extract',
162                               'run'])
163             else:
164                 # for some reason the installed tbb is newer than the current version?
165                 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
166
167         # not installed
168         else:
169             print _('TBB is not installed, attempting to install {0}'.format(latest_version))
170             self.set_gui('task', _("Downloading and installing Tor Browser for the first time."),
171                          ['download_sha256',
172                           'download_sha256_sig',
173                           'download_tarball',
174                           'verify',
175                           'extract',
176                           'run'])
177
178     # there are different GUIs that might appear, this sets which one we want
179     def set_gui(self, gui, message, tasks, autostart=True):
180         self.gui = gui
181         self.gui_message = message
182         self.gui_tasks = tasks
183         self.gui_task_i = 0
184         self.gui_autostart = autostart
185
186     # set all gtk variables to False
187     def clear_ui(self):
188         if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
189             self.box.destroy()
190         self.box = False
191
192         self.label = False
193         self.progressbar = False
194         self.button_box = False
195         self.start_button = False
196         self.exit_button = False
197
198     # build the application's UI
199     def build_ui(self):
200         self.clear_ui()
201
202         self.box = gtk.VBox(False, 20)
203         self.window.add(self.box)
204
205         if 'error' in self.gui:
206             # labels
207             self.label = gtk.Label(self.gui_message)
208             self.label.set_line_wrap(True)
209             self.box.pack_start(self.label, True, True, 0)
210             self.label.show()
211
212             # button box
213             self.button_box = gtk.HButtonBox()
214             self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
215             self.box.pack_start(self.button_box, True, True, 0)
216             self.button_box.show()
217
218             if self.gui != 'error':
219                 # yes button
220                 yes_image = gtk.Image()
221                 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
222                 self.yes_button = gtk.Button("Yes")
223                 self.yes_button.set_image(yes_image)
224                 if self.gui == 'error_try_stable':
225                     self.yes_button.connect("clicked", self.try_stable, None)
226                 elif self.gui == 'error_try_default_mirror':
227                     self.yes_button.connect("clicked", self.try_default_mirror, None)
228                 elif self.gui == 'error_try_tor':
229                     self.yes_button.connect("clicked", self.try_tor, None)
230                 self.button_box.add(self.yes_button)
231                 self.yes_button.show()
232
233             # exit button
234             exit_image = gtk.Image()
235             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
236             self.exit_button = gtk.Button("Exit")
237             self.exit_button.set_image(exit_image)
238             self.exit_button.connect("clicked", self.destroy, None)
239             self.button_box.add(self.exit_button)
240             self.exit_button.show()
241
242         elif self.gui == 'task':
243             # label
244             self.label = gtk.Label(self.gui_message)
245             self.label.set_line_wrap(True)
246             self.box.pack_start(self.label, True, True, 0)
247             self.label.show()
248
249             # progress bar
250             self.progressbar = gtk.ProgressBar(adjustment=None)
251             self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
252             self.progressbar.set_pulse_step(0.01)
253             self.box.pack_start(self.progressbar, True, True, 0)
254
255             # button box
256             self.button_box = gtk.HButtonBox()
257             self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
258             self.box.pack_start(self.button_box, True, True, 0)
259             self.button_box.show()
260
261             # start button
262             start_image = gtk.Image()
263             start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
264             self.start_button = gtk.Button(_("Start"))
265             self.start_button.set_image(start_image)
266             self.start_button.connect("clicked", self.start, None)
267             self.button_box.add(self.start_button)
268             if not self.gui_autostart:
269                 self.start_button.show()
270
271             # exit button
272             exit_image = gtk.Image()
273             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
274             self.exit_button = gtk.Button(_("Exit"))
275             self.exit_button.set_image(exit_image)
276             self.exit_button.connect("clicked", self.destroy, None)
277             self.button_box.add(self.exit_button)
278             self.exit_button.show()
279
280         self.box.show()
281         self.window.show()
282
283         if self.gui_autostart:
284             self.start(None)
285
286     # start button clicked, begin tasks
287     def start(self, widget, data=None):
288         # disable the start button
289         if self.start_button:
290             self.start_button.set_sensitive(False)
291
292         # start running tasks
293         self.run_task()
294
295     # run the next task in the task list
296     def run_task(self):
297         self.refresh_gtk()
298
299         if self.gui_task_i >= len(self.gui_tasks):
300             self.destroy(False)
301             return
302
303         task = self.gui_tasks[self.gui_task_i]
304
305         # get ready for the next task
306         self.gui_task_i += 1
307
308         print _('Running task: {0}'.format(task))
309         if task == 'download_update_check':
310             print _('Downloading'), self.common.paths['update_check_url']
311             self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
312
313         if task == 'attempt_update':
314             print _('Checking to see if update is needed')
315             self.attempt_update()
316
317         elif task == 'download_sha256':
318             print _('Downloading'), self.common.paths['sha256_url'].format(self.common.settings['mirror'])
319             self.download('signature', self.common.paths['sha256_url'], self.common.paths['sha256_file'])
320
321         elif task == 'download_sha256_sig':
322             print _('Downloading'), self.common.paths['sha256_sig_url'].format(self.common.settings['mirror'])
323             self.download('signature', self.common.paths['sha256_sig_url'], self.common.paths['sha256_sig_file'])
324
325         elif task == 'download_tarball':
326             print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
327             self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
328
329         elif task == 'verify':
330             print _('Verifying signature')
331             self.verify()
332
333         elif task == 'extract':
334             print _('Extracting'), self.common.paths['tarball_filename']
335             self.extract()
336
337         elif task == 'run':
338             print _('Running'), self.common.paths['tbb']['start']
339             self.run()
340
341         elif task == 'start_over':
342             print _('Starting download over again')
343             self.start_over()
344
345     def response_received(self, response):
346         class FileDownloader(Protocol):
347             def __init__(self, common, file, url, total, progress, done_cb):
348                 self.file = file
349                 self.total = total
350                 self.so_far = 0
351                 self.progress = progress
352                 self.all_done = done_cb
353
354                 if response.code != 200:
355                     if common.settings['mirror'] != common.default_mirror:
356                         raise TryDefaultMirrorException(_("Download Error: {0} {1}\n\nYou are currently using a non-default mirror:\n{2}\n\nWould you like to switch back to the default?").format(response.code, response.phrase, common.settings['mirror']))
357                     else:
358                         raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
359
360             def dataReceived(self, bytes):
361                 self.file.write(bytes)
362                 self.so_far += len(bytes)
363                 percent = float(self.so_far) / float(self.total)
364                 self.progress.set_fraction(percent)
365                 amount = float(self.so_far)
366                 units = "bytes"
367                 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
368                     if amount > size:
369                         units = unit
370                         amount = amount / float(size)
371                         break
372
373                 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
374
375             def connectionLost(self, reason):
376                 print _('Finished receiving body:'), reason.getErrorMessage()
377                 self.all_done(reason)
378
379         if hasattr(self, 'current_download_url'):
380             url = self.current_download_url
381         else:
382             url = None
383
384         dl = FileDownloader(self.common, self.file_download, url, response.length, self.progressbar, self.response_finished)
385         response.deliverBody(dl)
386
387     def response_finished(self, msg):
388         if msg.check(ResponseDone):
389             self.file_download.close()
390             delattr(self, 'current_download_path')
391             delattr(self, 'current_download_url')
392
393             # next task!
394             self.run_task()
395
396         else:
397             print "FINISHED", msg
398             ## FIXME handle errors
399
400     def download_error(self, f):
401         print _("Download error:"), f.value, type(f.value)
402
403         if isinstance(f.value, TryStableException):
404             f.trap(TryStableException)
405             self.set_gui('error_try_stable', str(f.value), [], False)
406
407         elif isinstance(f.value, TryDefaultMirrorException):
408             f.trap(TryDefaultMirrorException)
409             self.set_gui('error_try_default_mirror', str(f.value), [], False)
410
411         elif isinstance(f.value, DownloadErrorException):
412             f.trap(DownloadErrorException)
413             self.set_gui('error', str(f.value), [], False)
414
415         elif isinstance(f.value, DNSLookupError):
416             f.trap(DNSLookupError)
417             if common.settings['mirror'] != common.default_mirror:
418                 self.set_gui('error_try_default_mirror', _("DNS Lookup Error\n\nYou are currently using a non-default mirror:\n{0}\n\nWould you like to switch back to the default?").format(common.settings['mirror']), [], False)
419             else:
420                 self.set_gui('error', str(f.value), [], False)
421
422         elif isinstance(f.value, ResponseFailed):
423             for reason in f.value.reasons:
424                 if isinstance(reason.value, OpenSSL.SSL.Error):
425                     # TODO: add the ability to report attack by posting bug to trac.torproject.org
426                     if not self.common.settings['update_over_tor']:
427                         self.set_gui('error_try_tor', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack. Try the download again using Tor?'), [], False)
428                     else:
429                         self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
430
431         else:
432             self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
433
434         self.build_ui()
435
436     def download(self, name, url, path):
437         # keep track of current download
438         self.current_download_path = path
439         self.current_download_url = url
440
441         # initialize the progress bar
442         mirror_url = url.format(self.common.settings['mirror'])
443         self.progressbar.set_fraction(0)
444         self.progressbar.set_text(_('Downloading {0}').format(name))
445         self.progressbar.show()
446         self.refresh_gtk()
447
448         if self.common.settings['update_over_tor']:
449             print _('Updating over Tor')
450             from twisted.internet.endpoints import TCP4ClientEndpoint
451             from txsocksx.http import SOCKS5Agent
452
453             torEndpoint = TCP4ClientEndpoint(reactor, '127.0.0.1', 9050)
454
455             # default mirror gets certificate pinning, only for requests that use the mirror
456             if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
457                 agent = SOCKS5Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']), proxyEndpoint=torEndpoint)
458             else:
459                 agent = SOCKS5Agent(reactor, proxyEndpoint=torEndpoint)
460         else:
461             if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
462                 agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
463             else:
464                 agent = Agent(reactor)
465
466         # actually, agent needs to follow redirect
467         agent = RedirectAgent(agent)
468
469         # start the request
470         d = agent.request('GET', mirror_url,
471                           Headers({'User-Agent': ['torbrowser-launcher']}),
472                           None)
473
474         self.file_download = open(path, 'w')
475         d.addCallback(self.response_received).addErrback(self.download_error)
476
477         if not reactor.running:
478             reactor.run()
479
480     def try_default_mirror(self, widget, data=None):
481         # change mirror to default and relaunch TBL
482         self.common.settings['mirror'] = self.common.default_mirror
483         self.common.save_settings()
484         subprocess.Popen([self.common.paths['tbl_bin']])
485         self.destroy(False)
486
487     def try_tor(self, widget, data=None):
488         # set update_over_tor to true and relaunch TBL
489         self.common.settings['update_over_tor'] = True
490         self.common.save_settings()
491         subprocess.Popen([self.common.paths['tbl_bin']])
492         self.destroy(False)
493
494     def attempt_update(self):
495         # load the update check file
496         try:
497             versions = json.load(open(self.common.paths['update_check_file']))
498             latest = None
499
500             # filter linux versions
501             valid = []
502             for version in versions:
503                 if '-Linux' in version:
504                     valid.append(str(version))
505             valid.sort()
506             if len(valid):
507                 versions = valid
508
509             if len(versions) == 1:
510                 latest = versions.pop()
511             else:
512                 stable = []
513                 # remove alphas/betas
514                 for version in versions:
515                     if '-alpha-' not in version and '-beta-' not in version:
516                         stable.append(version)
517                 if len(stable):
518                     latest = stable.pop()
519                 else:
520                     latest = versions.pop()
521
522             if latest:
523                 latest = str(latest)
524                 if latest.endswith('-Linux'):
525                     latest = latest.rstrip('-Linux')
526
527                 self.common.settings['latest_version'] = latest
528                 self.common.settings['last_update_check_timestamp'] = int(time.time())
529                 self.common.settings['check_for_updates'] = False
530                 self.common.save_settings()
531                 self.common.build_paths(self.common.settings['latest_version'])
532                 self.start_launcher()
533
534             else:
535                 # failed to find the latest version
536                 self.set_gui('error', _("Error checking for updates."), [], False)
537
538         except:
539             # not a valid JSON object
540             self.set_gui('error', _("Error checking for updates."), [], False)
541
542         # now start over
543         self.clear_ui()
544         self.build_ui()
545
546     def verify(self):
547         # initialize the progress bar
548         self.progressbar.set_fraction(0)
549         self.progressbar.set_text(_('Verifying Signature'))
550         self.progressbar.show()
551
552         verified = False
553         # check the sha256 file's sig, and also take the sha256 of the tarball and compare
554         p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']])
555         self.pulse_until_process_exits(p)
556         if p.returncode == 0:
557             # compare with sha256 of the tarball
558             tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
559             for line in open(self.common.paths['sha256_file'], 'r').readlines():
560                 if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
561                     verified = True
562
563         if verified:
564             self.run_task()
565         else:
566             # TODO: add the ability to report attack by posting bug to trac.torproject.org
567             self.set_gui('task', _("SIGNATURE VERIFICATION FAILED!\n\nYou might be under attack, or there might just be a networking problem. Click Start try the download again."), ['start_over'], False)
568             self.clear_ui()
569             self.build_ui()
570
571             if not reactor.running:
572                 reactor.run()
573
574     def extract(self):
575         # initialize the progress bar
576         self.progressbar.set_fraction(0)
577         self.progressbar.set_text(_('Installing'))
578         self.progressbar.show()
579         self.refresh_gtk()
580
581         extracted = False
582         try:
583             if self.common.paths['tarball_file'][-2:] == 'xz':
584                 # if tarball is .tar.xz
585                 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
586                 tf = tarfile.open(fileobj=xz)
587                 tf.extractall(self.common.paths['tbb']['dir'])
588                 extracted = True
589             else:
590                 # if tarball is .tar.gz
591                 if tarfile.is_tarfile(self.common.paths['tarball_file']):
592                     tf = tarfile.open(self.common.paths['tarball_file'])
593                     tf.extractall(self.common.paths['tbb']['dir'])
594                     extracted = True
595         except:
596             pass
597
598         if not extracted:
599             self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
600             self.clear_ui()
601             self.build_ui()
602             return
603
604         # installation is finished, so save installed_version
605         self.common.settings['installed_version'] = self.common.settings['latest_version']
606         self.common.save_settings()
607
608         self.run_task()
609
610     def run(self, run_next_task=True):
611         # play modem sound?
612         if self.common.settings['modem_sound']:
613             def play_modem_sound():
614                 try:
615                     import pygame
616                     pygame.mixer.init()
617                     sound = pygame.mixer.Sound(self.common.paths['modem_sound'])
618                     sound.play()
619                     time.sleep(10)
620                 except ImportError:
621                     md = gtk.MessageDialog(None, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, _("The python-pygame package is missing, the modem sound is unavailable."))
622                     md.set_position(gtk.WIN_POS_CENTER)
623                     md.run()
624                     md.destroy()
625
626             t = threading.Thread(target=play_modem_sound)
627             t.start()
628
629         # hide the TBL window (#151)
630         self.window.hide()
631         while gtk.events_pending():
632             gtk.main_iteration_do(True)
633
634         # run Tor Browser
635         subprocess.call([self.common.paths['tbb']['start']])
636
637         if run_next_task:
638             self.run_task()
639
640     # make the progress bar pulse until process p (a Popen object) finishes
641     def pulse_until_process_exits(self, p):
642         while p.poll() is None:
643             time.sleep(0.01)
644             self.progressbar.pulse()
645             self.refresh_gtk()
646
647     # start over and download TBB again
648     def start_over(self):
649         self.label.set_text(_("Downloading Tor Browser Bundle over again."))
650         self.gui_tasks = ['download_tarball', 'verify', 'extract', 'run']
651         self.gui_task_i = 0
652         self.start(None)
653
654     # refresh gtk
655     def refresh_gtk(self):
656         while gtk.events_pending():
657             gtk.main_iteration(False)
658
659     # exit
660     def delete_event(self, widget, event, data=None):
661         return False
662
663     def destroy(self, widget, data=None):
664         if hasattr(self, 'file_download'):
665             self.file_download.close()
666         if hasattr(self, 'current_download_path'):
667             os.remove(self.current_download_path)
668             delattr(self, 'current_download_path')
669             delattr(self, 'current_download_url')
670         if reactor.running:
671             reactor.stop()
672