]> git.lizzy.rs Git - torbrowser-launcher.git/blob - torbrowser_launcher/launcher.py
split torbrowser-launcher into several files
[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
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."),
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."),
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                 if len(valid) == 1:
508                     latest = valid.pop()
509                 else:
510                     stable = []
511                     # remove alphas/betas
512                     for version in valid:
513                         if '-alpha-' not in version and '-beta-' not in version:
514                             stable.append(version)
515                     if len(stable):
516                         latest = stable.pop()
517                     else:
518                         latest = valid.pop()
519
520             if latest:
521                 self.common.settings['latest_version'] = latest[:-len('-Linux')]
522                 self.common.settings['last_update_check_timestamp'] = int(time.time())
523                 self.common.settings['check_for_updates'] = False
524                 self.common.save_settings()
525                 self.common.build_paths(self.common.settings['latest_version'])
526                 self.start_launcher()
527
528             else:
529                 # failed to find the latest version
530                 self.set_gui('error', _("Error checking for updates."), [], False)
531
532         except:
533             # not a valid JSON object
534             self.set_gui('error', _("Error checking for updates."), [], False)
535
536         # now start over
537         self.clear_ui()
538         self.build_ui()
539
540     def verify(self):
541         # initialize the progress bar
542         self.progressbar.set_fraction(0)
543         self.progressbar.set_text(_('Verifying Signature'))
544         self.progressbar.show()
545
546         verified = False
547         # check the sha256 file's sig, and also take the sha256 of the tarball and compare
548         p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']])
549         self.pulse_until_process_exits(p)
550         if p.returncode == 0:
551             # compare with sha256 of the tarball
552             tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
553             for line in open(self.common.paths['sha256_file'], 'r').readlines():
554                 if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
555                     verified = True
556
557         if verified:
558             self.run_task()
559         else:
560             # TODO: add the ability to report attack by posting bug to trac.torproject.org
561             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)
562             self.clear_ui()
563             self.build_ui()
564
565             if not reactor.running:
566                 reactor.run()
567
568     def extract(self):
569         # initialize the progress bar
570         self.progressbar.set_fraction(0)
571         self.progressbar.set_text(_('Installing'))
572         self.progressbar.show()
573         self.refresh_gtk()
574
575         extracted = False
576         try:
577             if self.common.paths['tarball_file'][-2:] == 'xz':
578                 # if tarball is .tar.xz
579                 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
580                 tf = tarfile.open(fileobj=xz)
581                 tf.extractall(self.common.paths['tbb']['dir'])
582                 extracted = True
583             else:
584                 # if tarball is .tar.gz
585                 if tarfile.is_tarfile(self.common.paths['tarball_file']):
586                     tf = tarfile.open(self.common.paths['tarball_file'])
587                     tf.extractall(self.common.paths['tbb']['dir'])
588                     extracted = True
589         except:
590             pass
591
592         if not extracted:
593             self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
594             self.clear_ui()
595             self.build_ui()
596             return
597
598         # installation is finished, so save installed_version
599         self.common.settings['installed_version'] = self.common.settings['latest_version']
600         self.common.save_settings()
601
602         self.run_task()
603
604     def run(self, run_next_task=True):
605         devnull = open('/dev/null', 'w')
606         subprocess.Popen([self.common.paths['tbb']['start']], stdout=devnull, stderr=devnull)
607
608         # play modem sound?
609         if self.common.settings['modem_sound']:
610             try:
611                 import pygame
612                 pygame.mixer.init()
613                 sound = pygame.mixer.Sound(self.common.paths['modem_sound'])
614                 sound.play()
615                 time.sleep(10)
616             except ImportError:
617                 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."))
618                 md.set_position(gtk.WIN_POS_CENTER)
619                 md.run()
620                 md.destroy()
621
622         if run_next_task:
623             self.run_task()
624
625     # make the progress bar pulse until process p (a Popen object) finishes
626     def pulse_until_process_exits(self, p):
627         while p.poll() is None:
628             time.sleep(0.01)
629             self.progressbar.pulse()
630             self.refresh_gtk()
631
632     # start over and download TBB again
633     def start_over(self):
634         self.label.set_text(_("Downloading Tor Browser Bundle over again."))
635         self.gui_tasks = ['download_tarball', 'verify', 'extract', 'run']
636         self.gui_task_i = 0
637         self.start(None)
638
639     # refresh gtk
640     def refresh_gtk(self):
641         while gtk.events_pending():
642             gtk.main_iteration(False)
643
644     # exit
645     def delete_event(self, widget, event, data=None):
646         return False
647
648     def destroy(self, widget, data=None):
649         if hasattr(self, 'file_download'):
650             self.file_download.close()
651         if hasattr(self, 'current_download_path'):
652             os.remove(self.current_download_path)
653             delattr(self, 'current_download_path')
654             delattr(self, 'current_download_url')
655         if reactor.running:
656             reactor.stop()
657