]> git.lizzy.rs Git - torbrowser-launcher.git/blob - torbrowser_launcher/launcher.py
6b89fbd4236240ff840f2ac37bc2ac25636317a6
[torbrowser-launcher.git] / torbrowser_launcher / launcher.py
1 """
2 Tor Browser Launcher
3 https://github.com/micahflee/torbrowser-launcher/
4
5 Copyright (c) 2013-2017 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
30 import subprocess
31 import time
32 import json
33 import tarfile
34 import hashlib
35 import lzma
36 import threading
37 import re
38 import unicodedata
39
40 from twisted.internet import reactor
41 from twisted.web.client import Agent, RedirectAgent, ResponseDone, ResponseFailed
42 from twisted.web.http_headers import Headers
43 from twisted.internet.protocol import Protocol
44 from twisted.internet.error import DNSLookupError, ConnectionRefusedError
45
46 try:
47     import gpg
48     gpgme_support = True
49 except ImportError:
50     gpgme_support = False
51
52 import xml.etree.ElementTree as ET
53
54 import OpenSSL
55
56 import pygtk
57 pygtk.require('2.0')
58 import gtk
59
60
61 class TryStableException(Exception):
62     pass
63
64
65 class TryDefaultMirrorException(Exception):
66     pass
67
68
69 class TryForcingEnglishException(Exception):
70     pass
71
72
73 class DownloadErrorException(Exception):
74     pass
75
76
77 class Launcher:
78     def __init__(self, common, url_list):
79         self.common = common
80         self.url_list = url_list
81         self.force_redownload = False
82
83         # this is the current version of Tor Browser, which should get updated with every release
84         self.min_version = '6.0.2'
85
86         # init launcher
87         self.set_gui(None, '', [])
88         self.launch_gui = True
89
90         # if Tor Browser is not installed, detect latest version, download, and install
91         if not self.common.settings['installed'] or not self.check_min_version():
92             # if downloading over Tor, include txsocksx
93             if self.common.settings['download_over_tor']:
94                 try:
95                     import txsocksx
96                     print _('Downloading over Tor')
97                 except ImportError:
98                     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"))
99                     md.set_position(gtk.WIN_POS_CENTER)
100                     md.run()
101                     md.destroy()
102                     self.common.settings['download_over_tor'] = False
103                     self.common.save_settings()
104
105             # different message if downloading for the first time, or because your installed version is too low
106             download_message = ""
107             if not self.common.settings['installed']:
108                 download_message = _("Downloading and installing Tor Browser for the first time.")
109             elif not self.check_min_version():
110                 download_message = _("Your version of Tor Browser is out-of-date. Downloading and installing the newest version.")
111
112             # download and install
113             print download_message
114             self.set_gui('task', download_message,
115                          ['download_version_check',
116                           'set_version',
117                           'download_sig',
118                           'download_tarball',
119                           'verify',
120                           'extract',
121                           'run'])
122
123         else:
124             # Tor Browser is already installed, so run
125             self.run(False)
126             self.launch_gui = False
127
128         if self.launch_gui:
129             # build the rest of the UI
130             self.build_ui()
131
132     def configure_window(self):
133         if not hasattr(self, 'window'):
134             self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
135             self.window.set_title(_("Tor Browser"))
136             self.window.set_icon_from_file(self.common.paths['icon_file'])
137             self.window.set_position(gtk.WIN_POS_CENTER)
138             self.window.set_border_width(10)
139             self.window.connect("delete_event", self.delete_event)
140             self.window.connect("destroy", self.destroy)
141
142     # there are different GUIs that might appear, this sets which one we want
143     def set_gui(self, gui, message, tasks, autostart=True):
144         self.gui = gui
145         self.gui_message = message
146         self.gui_tasks = tasks
147         self.gui_task_i = 0
148         self.gui_autostart = autostart
149
150     # set all gtk variables to False
151     def clear_ui(self):
152         if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
153             self.box.destroy()
154         self.box = False
155
156         self.label = False
157         self.progressbar = False
158         self.button_box = False
159         self.start_button = False
160         self.exit_button = False
161
162     # build the application's UI
163     def build_ui(self):
164         self.clear_ui()
165
166         self.box = gtk.VBox(False, 20)
167         self.configure_window()
168         self.window.add(self.box)
169
170         if 'error' in self.gui:
171             # labels
172             self.label = gtk.Label(self.gui_message)
173             self.label.set_line_wrap(True)
174             self.box.pack_start(self.label, True, True, 0)
175             self.label.show()
176
177             # button box
178             self.button_box = gtk.HButtonBox()
179             self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
180             self.box.pack_start(self.button_box, True, True, 0)
181             self.button_box.show()
182
183             if self.gui != 'error':
184                 # yes button
185                 yes_image = gtk.Image()
186                 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
187                 self.yes_button = gtk.Button("Yes")
188                 self.yes_button.set_image(yes_image)
189                 if self.gui == 'error_try_stable':
190                     self.yes_button.connect("clicked", self.try_stable, None)
191                 elif self.gui == 'error_try_default_mirror':
192                     self.yes_button.connect("clicked", self.try_default_mirror, None)
193                 elif self.gui == 'error_try_forcing_english':
194                     self.yes_button.connect("clicked", self.try_forcing_english, None)
195                 elif self.gui == 'error_try_tor':
196                     self.yes_button.connect("clicked", self.try_tor, None)
197                 self.button_box.add(self.yes_button)
198                 self.yes_button.show()
199
200             # exit button
201             exit_image = gtk.Image()
202             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
203             self.exit_button = gtk.Button("Exit")
204             self.exit_button.set_image(exit_image)
205             self.exit_button.connect("clicked", self.destroy, None)
206             self.button_box.add(self.exit_button)
207             self.exit_button.show()
208
209         elif self.gui == 'task':
210             # label
211             self.label = gtk.Label(self.gui_message)
212             self.label.set_line_wrap(True)
213             self.box.pack_start(self.label, True, True, 0)
214             self.label.show()
215
216             # progress bar
217             self.progressbar = gtk.ProgressBar(adjustment=None)
218             self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
219             self.progressbar.set_pulse_step(0.01)
220             self.box.pack_start(self.progressbar, True, True, 0)
221
222             # button box
223             self.button_box = gtk.HButtonBox()
224             self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
225             self.box.pack_start(self.button_box, True, True, 0)
226             self.button_box.show()
227
228             # start button
229             start_image = gtk.Image()
230             start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
231             self.start_button = gtk.Button(_("Start"))
232             self.start_button.set_image(start_image)
233             self.start_button.connect("clicked", self.start, None)
234             self.button_box.add(self.start_button)
235             if not self.gui_autostart:
236                 self.start_button.show()
237
238             # exit button
239             exit_image = gtk.Image()
240             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
241             self.exit_button = gtk.Button(_("Cancel"))
242             self.exit_button.set_image(exit_image)
243             self.exit_button.connect("clicked", self.destroy, None)
244             self.button_box.add(self.exit_button)
245             self.exit_button.show()
246
247         self.box.show()
248         self.window.show()
249
250         if self.gui_autostart:
251             self.start(None)
252
253     # start button clicked, begin tasks
254     def start(self, widget, data=None):
255         # disable the start button
256         if self.start_button:
257             self.start_button.set_sensitive(False)
258
259         # start running tasks
260         self.run_task()
261
262     # run the next task in the task list
263     def run_task(self):
264         self.refresh_gtk()
265
266         if self.gui_task_i >= len(self.gui_tasks):
267             self.destroy(False)
268             return
269
270         task = self.gui_tasks[self.gui_task_i]
271
272         # get ready for the next task
273         self.gui_task_i += 1
274
275         if task == 'download_version_check':
276             print _('Downloading'), self.common.paths['version_check_url']
277             self.download('version check', self.common.paths['version_check_url'],
278                           self.common.paths['version_check_file'])
279
280         if task == 'set_version':
281             version = self.get_stable_version()
282             if version:
283                 self.common.build_paths(self.get_stable_version())
284                 print _('Latest version: {}').format(version)
285                 self.run_task()
286             else:
287                 self.set_gui('error', _("Error detecting Tor Browser version."), [], False)
288                 self.clear_ui()
289                 self.build_ui()
290
291         elif task == 'download_sig':
292             print _('Downloading'), self.common.paths['sig_url'].format(self.common.settings['mirror'])
293             self.download('signature', self.common.paths['sig_url'], self.common.paths['sig_file'])
294
295         elif task == 'download_tarball':
296             print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
297             if not self.force_redownload and os.path.exists(self.common.paths['tarball_file']):
298                 self.run_task()
299             else:
300                 self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
301
302         elif task == 'verify':
303             print _('Verifying Signature')
304             self.verify()
305
306         elif task == 'extract':
307             print _('Extracting'), self.common.paths['tarball_filename']
308             self.extract()
309
310         elif task == 'run':
311             print _('Running'), self.common.paths['tbb']['start']
312             self.run()
313
314         elif task == 'start_over':
315             print _('Starting download over again')
316             self.start_over()
317
318     def response_received(self, response):
319         class FileDownloader(Protocol):
320             def __init__(self, common, file, url, total, progress, done_cb):
321                 self.file = file
322                 self.total = total
323                 self.so_far = 0
324                 self.progress = progress
325                 self.all_done = done_cb
326
327                 if response.code != 200:
328                     if common.settings['mirror'] != common.default_mirror:
329                         raise TryDefaultMirrorException(
330                             (_("Download Error:") + " {0} {1}\n\n" + _("You are currently using a non-default mirror")
331                              + ":\n{2}\n\n" + _("Would you like to switch back to the default?")).format(
332                                 response.code, response.phrase, common.settings['mirror']
333                             )
334                         )
335                     elif common.language != 'en-US' and not common.settings['force_en-US']:
336                         raise TryForcingEnglishException(
337                             (_("Download Error:") + " {0} {1}\n\n"
338                              + _("Would you like to try the English version of Tor Browser instead?")).format(
339                                 response.code, response.phrase
340                             )
341                         )
342                     else:
343                         raise DownloadErrorException(
344                             (_("Download Error:") + " {0} {1}").format(response.code, response.phrase)
345                         )
346
347             def dataReceived(self, bytes):
348                 self.file.write(bytes)
349                 self.so_far += len(bytes)
350                 percent = float(self.so_far) / float(self.total)
351                 self.progress.set_fraction(percent)
352                 amount = float(self.so_far)
353                 units = "bytes"
354                 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
355                     if amount > size:
356                         units = unit
357                         amount /= float(size)
358                         break
359
360                 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
361
362             def connectionLost(self, reason):
363                 self.all_done(reason)
364
365         if hasattr(self, 'current_download_url'):
366             url = self.current_download_url
367         else:
368             url = None
369
370         dl = FileDownloader(
371             self.common, self.file_download, url, response.length, self.progressbar, self.response_finished
372         )
373         response.deliverBody(dl)
374
375     def response_finished(self, msg):
376         if msg.check(ResponseDone):
377             self.file_download.close()
378             delattr(self, 'current_download_path')
379             delattr(self, 'current_download_url')
380
381             # next task!
382             self.run_task()
383
384         else:
385             print "FINISHED", msg
386             # FIXME handle errors
387
388     def download_error(self, f):
389         print _("Download Error:"), f.value, type(f.value)
390
391         if isinstance(f.value, TryStableException):
392             f.trap(TryStableException)
393             self.set_gui('error_try_stable', str(f.value), [], False)
394
395         elif isinstance(f.value, TryDefaultMirrorException):
396             f.trap(TryDefaultMirrorException)
397             self.set_gui('error_try_default_mirror', str(f.value), [], False)
398
399         elif isinstance(f.value, TryForcingEnglishException):
400             f.trap(TryForcingEnglishException)
401             self.set_gui('error_try_forcing_english', str(f.value), [], False)
402
403         elif isinstance(f.value, DownloadErrorException):
404             f.trap(DownloadErrorException)
405             self.set_gui('error', str(f.value), [], False)
406
407         elif isinstance(f.value, DNSLookupError):
408             f.trap(DNSLookupError)
409             if common.settings['mirror'] != common.default_mirror:
410                 self.set_gui('error_try_default_mirror', (_("DNS Lookup Error") + "\n\n" +
411                                                           _("You are currently using a non-default mirror")
412                                                           + ":\n{0}\n\n"
413                                                           + _("Would you like to switch back to the default?")
414                                                           ).format(common.settings['mirror']), [], False)
415             else:
416                 self.set_gui('error', str(f.value), [], False)
417
418         elif isinstance(f.value, ResponseFailed):
419             for reason in f.value.reasons:
420                 if isinstance(reason.value, OpenSSL.SSL.Error):
421                     # TODO: add the ability to report attack by posting bug to trac.torproject.org
422                     if not self.common.settings['download_over_tor']:
423                         self.set_gui('error_try_tor',
424                                      _('The SSL certificate served by https://www.torproject.org is invalid! You may '
425                                        'be under attack.') + " " + _('Try the download again using Tor?'), [], False)
426                     else:
427                         self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! '
428                                                 'You may be under attack.'), [], False)
429
430         elif isinstance(f.value, ConnectionRefusedError) and self.common.settings['download_over_tor']:
431             # If we're using Tor, we'll only get this error when we fail to
432             # connect to the SOCKS server.  If the connection fails at the
433             # remote end, we'll get txsocksx.errors.ConnectionRefused.
434             addr = self.common.settings['tor_socks_address']
435             self.set_gui('error', _("Error connecting to Tor at {0}").format(addr), [], False)
436
437         else:
438             self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
439
440         self.build_ui()
441
442     def download(self, name, url, path):
443         # keep track of current download
444         self.current_download_path = path
445         self.current_download_url = url
446
447         mirror_url = url.format(self.common.settings['mirror'])
448
449         # convert mirror_url from unicode to string, if needed (#205)
450         if isinstance(mirror_url, unicode):
451             mirror_url = unicodedata.normalize('NFKD', mirror_url).encode('ascii', 'ignore')
452
453         # initialize the progress bar
454         self.progressbar.set_fraction(0)
455         self.progressbar.set_text(_('Downloading') + ' {0}'.format(name))
456         self.progressbar.show()
457         self.refresh_gtk()
458
459         if self.common.settings['download_over_tor']:
460             from twisted.internet.endpoints import clientFromString
461             from txsocksx.http import SOCKS5Agent
462
463             torendpoint = clientFromString(reactor, self.common.settings['tor_socks_address'])
464
465             # default mirror gets certificate pinning, only for requests that use the mirror
466             agent = SOCKS5Agent(reactor, proxyEndpoint=torendpoint)
467         else:
468             agent = Agent(reactor)
469
470         # actually, agent needs to follow redirect
471         agent = RedirectAgent(agent)
472
473         # start the request
474         d = agent.request('GET', mirror_url,
475                           Headers({'User-Agent': ['torbrowser-launcher']}),
476                           None)
477
478         self.file_download = open(path, 'w')
479         d.addCallback(self.response_received).addErrback(self.download_error)
480
481         if not reactor.running:
482             reactor.run()
483
484     def try_default_mirror(self, widget, data=None):
485         # change mirror to default and relaunch TBL
486         self.common.settings['mirror'] = self.common.default_mirror
487         self.common.save_settings()
488         subprocess.Popen([self.common.paths['tbl_bin']])
489         self.destroy(False)
490
491     def try_forcing_english(self, widget, data=None):
492         # change force english to true and relaunch TBL
493         self.common.settings['force_en-US'] = True
494         self.common.save_settings()
495         subprocess.Popen([self.common.paths['tbl_bin']])
496         self.destroy(False)
497
498     def try_tor(self, widget, data=None):
499         # set download_over_tor to true and relaunch TBL
500         self.common.settings['download_over_tor'] = True
501         self.common.save_settings()
502         subprocess.Popen([self.common.paths['tbl_bin']])
503         self.destroy(False)
504
505     def get_stable_version(self):
506         tree = ET.parse(self.common.paths['version_check_file'])
507         for up in tree.getroot():
508             if up.tag == 'update' and up.attrib['appVersion']:
509                 version = str(up.attrib['appVersion'])
510
511                 # make sure the version does not contain directory traversal attempts
512                 # e.g. "5.5.3", "6.0a", "6.0a-hardened" are valid but "../../../../.." is invalid
513                 if not re.match(r'^[a-z0-9\.\-]+$', version):
514                     return None
515
516                 return version
517         return None
518
519     def verify(self):
520         self.progressbar.set_fraction(0)
521         self.progressbar.set_text(_('Verifying Signature'))
522         self.progressbar.show()
523
524         def gui_raise_sigerror(self, sigerror='MissingErr'):
525             """
526             :type sigerror: str
527             """
528             sigerror = 'SIGNATURE VERIFICATION FAILED!\n\nError Code: {0}\n\nYou might be under attack, there might' \
529                        ' be a network\nproblem, or you may be missing a recently added\nTor Browser verification key.' \
530                        '\n\nFor support, report the above error code.\nClick Start to try again.'.format(sigerror)
531             self.set_gui('task', sigerror, ['start_over'], False)
532             self.clear_ui()
533             self.build_ui()
534
535         if gpgme_support:
536             with gpg.Context() as c:
537                 c.set_engine_info(gpg.constants.protocol.OpenPGP, home_dir=self.common.paths['gnupg_homedir'])
538
539                 sig = gpg.Data(file=self.common.paths['sig_file'])
540                 signed = gpg.Data(file=self.common.paths['tarball_file'])
541
542                 try:
543                     c.verify(signature=sig, signed_data=signed)
544                 except gpg.errors.BadSignatures as e:
545                     result = str(e).split(": ")
546                     if result[1] == 'Bad signature':
547                         gui_raise_sigerror(self, str(e))
548                     elif result[1] == 'No public key':
549                         gui_raise_sigerror(self, str(e))
550                 else:
551                     self.run_task()
552         else:
553             FNULL = open(os.devnull, 'w')
554             p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify',
555                                   self.common.paths['sig_file'], self.common.paths['tarball_file']], stdout=FNULL,
556                                  stderr=subprocess.STDOUT)
557             self.pulse_until_process_exits(p)
558             if p.returncode == 0:
559                 self.run_task()
560             else:
561                 gui_raise_sigerror(self, 'VERIFY_FAIL_NO_GPGME')
562                 if not reactor.running:
563                     reactor.run()
564
565     def extract(self):
566         # initialize the progress bar
567         self.progressbar.set_fraction(0)
568         self.progressbar.set_text(_('Installing'))
569         self.progressbar.show()
570         self.refresh_gtk()
571
572         extracted = False
573         try:
574             if self.common.paths['tarball_file'][-2:] == 'xz':
575                 # if tarball is .tar.xz
576                 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
577                 tf = tarfile.open(fileobj=xz)
578                 tf.extractall(self.common.paths['tbb']['dir'])
579                 extracted = True
580             else:
581                 # if tarball is .tar.gz
582                 if tarfile.is_tarfile(self.common.paths['tarball_file']):
583                     tf = tarfile.open(self.common.paths['tarball_file'])
584                     tf.extractall(self.common.paths['tbb']['dir'])
585                     extracted = True
586         except:
587             pass
588
589         if not extracted:
590             self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
591             self.clear_ui()
592             self.build_ui()
593             return
594
595         self.run_task()
596
597     def check_min_version(self):
598         installed_version = None
599         for line in open(self.common.paths['tbb']['versions']).readlines():
600             if line.startswith('TORBROWSER_VERSION='):
601                 installed_version = line.split('=')[1].strip()
602                 break
603
604         if self.min_version <= installed_version:
605             return True
606
607         return False
608
609     def run(self, run_next_task=True):
610         # don't run if it isn't at least the minimum version
611         if not self.check_min_version():
612             message = _("The version of Tor Browser you have installed is earlier than it should be, which could be a "
613                         "sign of an attack!")
614             print message
615
616             md = gtk.MessageDialog(None, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, _(message))
617             md.set_position(gtk.WIN_POS_CENTER)
618             md.run()
619             md.destroy()
620
621             return
622
623         # play modem sound?
624         if self.common.settings['modem_sound']:
625             def play_modem_sound():
626                 try:
627                     import pygame
628                     pygame.mixer.init()
629                     sound = pygame.mixer.Sound(self.common.paths['modem_sound'])
630                     sound.play()
631                     time.sleep(10)
632                 except ImportError:
633                     md = gtk.MessageDialog(
634                         None, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE,
635                         _("The python-pygame package is missing, the modem sound is unavailable.")
636                     )
637                     md.set_position(gtk.WIN_POS_CENTER)
638                     md.run()
639                     md.destroy()
640
641             t = threading.Thread(target=play_modem_sound)
642             t.start()
643
644         # hide the TBL window (#151)
645         if hasattr(self, 'window'):
646             self.window.hide()
647             while gtk.events_pending():
648                 gtk.main_iteration_do(True)
649
650         # run Tor Browser
651         subprocess.call([self.common.paths['tbb']['start']], cwd=self.common.paths['tbb']['dir_tbb'])
652
653         if run_next_task:
654             self.run_task()
655
656     # make the progress bar pulse until process p (a Popen object) finishes
657     def pulse_until_process_exits(self, p):
658         while p.poll() is None:
659             time.sleep(0.01)
660             self.progressbar.pulse()
661             self.refresh_gtk()
662
663     # start over and download TBB again
664     def start_over(self):
665         self.force_redownload = True  # Overwrite any existing file
666         self.label.set_text(_("Downloading Tor Browser Bundle over again."))
667         self.gui_tasks = ['download_tarball', 'verify', 'extract', 'run']
668         self.gui_task_i = 0
669         self.start(None)
670
671     # refresh gtk
672     def refresh_gtk(self):
673         while gtk.events_pending():
674             gtk.main_iteration(False)
675
676     # exit
677     def delete_event(self, widget, event, data=None):
678         return False
679
680     def destroy(self, widget, data=None):
681         if hasattr(self, 'file_download'):
682             self.file_download.close()
683         if hasattr(self, 'current_download_path'):
684             os.remove(self.current_download_path)
685             delattr(self, 'current_download_path')
686             delattr(self, 'current_download_url')
687         if reactor.running:
688             reactor.stop()