]> git.lizzy.rs Git - torbrowser-launcher.git/blob - torbrowser-launcher
canceling a download deletes the file, fixes #44
[torbrowser-launcher.git] / torbrowser-launcher
1 #!/usr/bin/env python
2 """
3 Tor Browser Launcher
4 https://github.com/micahflee/torbrowser-launcher/
5
6 Copyright (c) 2013 Micah Lee <micahflee@riseup.net>
7
8 Permission is hereby granted, free of charge, to any person
9 obtaining a copy of this software and associated documentation
10 files (the "Software"), to deal in the Software without
11 restriction, including without limitation the rights to use,
12 copy, modify, merge, publish, distribute, sublicense, and/or sell
13 copies of the Software, and to permit persons to whom the
14 Software is furnished to do so, subject to the following
15 conditions:
16
17 The above copyright notice and this permission notice shall be
18 included in all copies or substantial portions of the Software.
19
20 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
22 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
24 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
25 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27 OTHER DEALINGS IN THE SOFTWARE.
28 """
29
30 import sys
31 sys.path.append('/usr/share/torbrowser-launcher/lib/')
32
33 import gettext
34 gettext.install('torbrowser-launcher', '/usr/share/torbrowser-launcher/locale')
35
36 from twisted.internet import gtk2reactor
37 gtk2reactor.install()
38 from twisted.internet import reactor
39
40 import pygtk
41 pygtk.require('2.0')
42 import gtk
43
44 import os, subprocess, locale, urllib2, gobject, time, pickle, json, tarfile, psutil
45
46 from twisted.web.client import Agent, RedirectAgent, ResponseDone, ResponseFailed
47 from twisted.web.http_headers import Headers
48 from twisted.internet.protocol import Protocol
49 from twisted.internet.ssl import ClientContextFactory
50 from twisted.internet.endpoints import TCP4ClientEndpoint
51 from twisted.internet.error import DNSLookupError
52
53 from txsocksx.client import SOCKS5ClientEndpoint
54
55 import OpenSSL
56
57 class TryStableException(Exception):
58     pass
59 class TryDefaultMirrorException(Exception):
60     pass
61 class DownloadErrorException(Exception):
62     pass
63
64 class VerifyTorProjectCert(ClientContextFactory):
65
66     def __init__(self, torproject_pem):
67         self.torproject_ca = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, open(torproject_pem, 'r').read())
68
69     def getContext(self, host, port):
70         ctx = ClientContextFactory.getContext(self)
71         ctx.set_verify_depth(0)
72         ctx.set_verify(OpenSSL.SSL.VERIFY_PEER | OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname)
73         return ctx
74
75     def verifyHostname(self, connection, cert, errno, depth, preverifyOK):
76         return cert.digest('sha256') == self.torproject_ca.digest('sha256')
77
78 class TBLCommon:
79
80     def __init__(self, tbl_version):
81         print _('Initializing Tor Browser Launcher')
82         self.tbl_version = tbl_version
83
84         # initialize the app
85         self.available_versions = {
86             'stable': _('Tor Browser Bundle - stable'), 
87             'alpha': _('Tor Browser Bundle - alpha')
88         }
89         self.default_mirror = 'https://www.torproject.org/dist/'
90         
91         self.discover_arch_lang()
92         self.build_paths()
93         self.mkdir(self.paths['data_dir'])
94         self.load_mirrors()
95         self.load_settings()
96         self.mkdir(self.paths['download_dir'])
97         self.mkdir(self.paths['tbb'][self.settings['preferred']]['dir'])
98         self.init_gnupg()
99
100         # allow buttons to have icons
101         try:
102             gtk_settings = gtk.settings_get_default()
103             gtk_settings.props.gtk_button_images = True
104         except:
105             pass
106
107     # discover the architecture and language
108     def discover_arch_lang(self):
109         # figure out the architecture
110         (sysname, nodename, release, version, machine) = os.uname()
111         self.architecture = machine
112
113         # figure out the language
114         available_languages = ['en-US', 'ar', 'de', 'es-ES', 'fa', 'fr', 'it', 'ko', 'nl', 'pl', 'pt-PT', 'ru', 'vi', 'zh-CN']
115         default_locale = locale.getdefaultlocale()[0]
116         if default_locale == None:
117             self.language = 'en-US'
118         else:
119             self.language = default_locale.replace('_', '-')
120             if self.language not in available_languages:
121                 self.language = self.language.split('-')[0]
122                 if self.language not in available_languages:
123                     for l in available_languages:
124                         if l[0:2] == self.language:
125                             self.language = l
126             # if language isn't available, default to english
127             if self.language not in available_languages:
128                 self.language = 'en-US'
129
130     # build all relevant paths
131     def build_paths(self, tbb_version = None):
132         homedir = os.getenv('HOME')
133         if not homedir:
134             homedir = '/tmp/.torbrowser-'+os.getenv('USER')
135             if os.path.exists(homedir) == False:
136                 try:
137                     os.mkdir(homedir, 0700)
138                 except:
139                     self.set_gui('error', _("Error creating {0}").format(homedir), [], False)
140         if not os.access(homedir, os.W_OK):
141             self.set_gui('error', _("{0} is not writable").format(homedir), [], False)
142
143         tbb_data = '%s/.torbrowser' % homedir
144
145         if tbb_version:
146             tarball_filename = 'tor-browser-gnu-linux-'+self.architecture+'-'+tbb_version+'-dev-'+self.language+'.tar.gz'
147             self.paths['tarball_file'] = tbb_data+'/download/'+tarball_filename
148             self.paths['tarball_sig_file'] = tbb_data+'/download/'+tarball_filename+'.asc'
149             self.paths['tarball_url'] = '{0}torbrowser/linux/'+tarball_filename # {0} will be replaced with the mirror
150             self.paths['tarball_sig_url'] = '{0}torbrowser/linux/'+tarball_filename+'.asc'
151             self.paths['tarball_filename'] = tarball_filename
152             self.paths['tarball_sig_filename'] = tarball_filename+'.asc'
153
154         else:
155             self.paths = {
156                 'tbl_bin': '/usr/bin/torbrowser-launcher',
157                 'icon_file': '/usr/share/pixmaps/torbrowser80.xpm',
158                 'torproject_pem': '/usr/share/torbrowser-launcher/torproject.pem',
159                 'erinn_key': '/usr/share/torbrowser-launcher/erinn.asc',
160                 'sebastian_key': '/usr/share/torbrowser-launcher/sebastian.asc',
161                 'alexandre_key': '/usr/share/torbrowser-launcher/alexandre.asc',
162                 'mirrors_txt': '/usr/share/torbrowser-launcher/mirrors.txt',
163                 'data_dir': tbb_data,
164                 'download_dir': tbb_data+'/download',
165                 'gnupg_homedir': tbb_data+'/gnupg_homedir',
166                 'settings_file': tbb_data+'/settings',
167                 'update_check_url': 'https://check.torproject.org/RecommendedTBBVersions',
168                 'update_check_file': tbb_data+'/download/RecommendedTBBVersions',
169                 'tbb': {
170                     'stable': {
171                         'dir': tbb_data+'/tbb/stable/'+self.architecture,
172                         'start': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
173                         'vidalia_bin': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/App/vidalia',
174                         'firefox_bin': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/App/Firefox/firefox',
175                         'firefox_profile': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile',
176                     },
177                     'alpha': {
178                         'dir': tbb_data+'/tbb/alpha/'+self.architecture,
179                         'start': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
180                         'vidalia_bin': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/App/vidalia',
181                         'firefox_bin': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/App/Firefox/firefox',
182                         'firefox_profile': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/Data/profile',
183                     }
184                 }
185             }
186
187     # create a directory
188     def mkdir(self, path):
189         try:
190             if os.path.exists(path) == False:
191                 os.makedirs(path, 0700)
192                 return True
193         except:
194             print _("Cannot create directory {0}").format(path)
195             return False
196         if not os.access(path, os.W_OK):
197             print _("{0} is not writable").format(path)
198             return False
199         return True
200
201     # if gnupg_homedir isn't set up, set it up
202     def init_gnupg(self):
203         if not os.path.exists(self.paths['gnupg_homedir']):
204             print _('Creating GnuPG homedir'), self.paths['gnupg_homedir']
205             if self.mkdir(self.paths['gnupg_homedir']):
206                 # import keys
207                 print _('Importing keys')
208                 subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['gnupg_homedir'], '--import', self.paths['erinn_key']]).wait()
209                 subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['gnupg_homedir'], '--import', self.paths['sebastian_key']]).wait()
210                 subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['gnupg_homedir'], '--import', self.paths['alexandre_key']]).wait()
211
212     # load mirrors
213     def load_mirrors(self):
214         self.mirrors = []
215         for mirror in open(self.paths['mirrors_txt'], 'r').readlines():
216             self.mirrors.append(mirror.strip())
217     
218     # load settings
219     def load_settings(self):
220         default_settings = {
221             'tbl_version': self.tbl_version,
222             'preferred': 'stable',
223             'installed_version': {
224                 'stable': False,
225                 'alpha': False
226             },
227             'latest_version': {
228                 'stable': '0',
229                 'alpha': '0'
230             },
231             'update_over_tor': True,
232             'check_for_updates': False,
233             'last_update_check_timestamp': 0,
234             'mirror': self.default_mirror 
235         }
236
237         if os.path.isfile(self.paths['settings_file']):
238             settings = pickle.load(open(self.paths['settings_file']))
239
240             # what version settings is this?
241             if not 'tbl_version' in settings:
242                 settings['tbl_version'] = '0.0.1'
243
244             # sanity checks for current version
245             if settings['tbl_version'] == self.tbl_version:
246                 good_settings = True
247                 if not 'preferred' in settings:
248                     good_settings = False
249                 if not 'installed_version' in settings:
250                     good_settings = False
251                 if not 'stable' in settings['installed_version']:
252                     good_settings = False
253                 if not 'alpha' in settings['installed_version']:
254                     good_settings = False
255                 if not 'latest_version' in settings:
256                     good_settings = False
257                 if not 'stable' in settings['latest_version']:
258                     good_settings = False
259                 if not 'alpha' in settings['latest_version']:
260                     good_settings = False
261                 if not 'update_over_tor' in settings:
262                     good_settings = False
263                 if not 'check_for_updates' in settings:
264                     good_settings = False
265                 if not 'last_update_check_timestamp' in settings:
266                     good_settings = False
267                 if not 'mirror' in settings:
268                     good_settings = False
269
270                 if good_settings:
271                     self.settings = settings
272                 else:
273                     self.settings = default_settings
274
275             # settings migrations for previous versions
276             elif settings['tbl_version'] == '0.0.1':
277                 self.settings = default_settings
278                 self.settings['installed_version']['alpha'] = settings['installed_version']
279                 self.save_settings()
280
281                 # move tbb alpha
282                 self.mkdir(self.paths['tbb']['alpha']['dir'])
283                 # todo: move already-installed TBB alpha to new location
284                 if os.path.exists(self.paths['data_dir']+'/tbb/x86_64'):
285                     pass
286                 if os.path.exists(self.paths['data_dir']+'/tbb/i686'):
287                     pass
288
289         else:
290             self.settings = default_settings
291             self.save_settings()
292
293     # save settings
294     def save_settings(self):
295         pickle.dump(self.settings, open(self.paths['settings_file'], 'w'))
296         return True
297
298     # get the process id of a program
299     def get_pid(self, bin_path, python = False):
300         pid = None
301
302         for p in psutil.process_iter():
303             try:
304                 if p.pid != os.getpid():
305                     exe = None
306                     if python:
307                         if len(p.cmdline) > 1:
308                             if 'python' in p.cmdline[0]:
309                                 exe = p.cmdline[1]
310                     else:
311                         if len(p.cmdline) > 0:
312                             exe = p.cmdline[0]
313                     
314                     if exe == bin_path:
315                         pid = p.pid
316
317             except:
318                 pass
319
320         return pid
321
322     # bring program's x window to front
323     def bring_window_to_front(self, pid):
324         # figure out the window id
325         win_id = None
326         p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
327         for line in p.stdout.readlines():
328             line_split = line.split()
329             cur_win_id = line_split[0]
330             cur_win_pid = int(line_split[2])
331             if cur_win_pid == pid:
332                 win_id = cur_win_id
333
334         # bring to front
335         if win_id:
336             subprocess.call(['wmctrl', '-i', '-a', win_id])
337
338 class TBLSettings:
339     def __init__(self, common):
340         print _('Starting settings dialog')
341         self.common = common
342
343         # set up the window
344         self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
345         self.window.set_title(_("Tor Browser Launcher Settings"))
346         self.window.set_icon_from_file(self.common.paths['icon_file'])
347         self.window.set_position(gtk.WIN_POS_CENTER)
348         self.window.set_border_width(10)
349         self.window.connect("delete_event", self.delete_event)
350         self.window.connect("destroy", self.destroy)
351         
352         # build the rest of the UI
353         self.box = gtk.VBox(False, 10)
354         self.window.add(self.box)
355         self.box.show()
356
357         self.hbox = gtk.HBox(False, 10)
358         self.box.pack_start(self.hbox, True, True, 0)
359         self.hbox.show()
360
361         self.settings_box = gtk.VBox(False, 10)
362         self.hbox.pack_start(self.settings_box, True, True, 0)
363         self.settings_box.show()
364
365         self.labels_box = gtk.VBox(False, 10)
366         self.hbox.pack_start(self.labels_box, True, True, 0)
367         self.labels_box.show()
368
369         # preferred version
370         self.preferred_box = gtk.HBox(False, 10)
371         self.settings_box.pack_start(self.preferred_box, True, True, 0)
372         self.preferred_box.show()
373
374         self.preferred_label = gtk.Label(_('I prefer'))
375         self.preferred_label.set_line_wrap(True)
376         self.preferred_box.pack_start(self.preferred_label, True, True, 0)
377         self.preferred_label.show()
378
379         self.preferred_options = []
380         for i in self.common.available_versions:
381             self.preferred_options.append(self.common.available_versions[i])
382         self.preferred_options.sort()
383
384         self.preferred = gtk.combo_box_new_text()
385         for option in self.preferred_options:
386             self.preferred.append_text(option)
387         if self.common.settings['preferred'] in self.common.available_versions:
388             self.preferred.set_active( self.preferred_options.index(self.common.available_versions[self.common.settings['preferred']]) )
389         else:
390             self.preferred.set_active(0)
391         self.preferred_box.pack_start(self.preferred, True, True, 0)
392         self.preferred.show()
393
394         # download over tor
395         self.tor_update_checkbox = gtk.CheckButton(_("Download updates over Tor (recommended)"))
396         self.settings_box.pack_start(self.tor_update_checkbox, True, True, 0)
397         if self.common.settings['update_over_tor']:
398             self.tor_update_checkbox.set_active(True) 
399         else:
400             self.tor_update_checkbox.set_active(False) 
401         self.tor_update_checkbox.show()
402
403         # check for updates
404         self.update_checkbox = gtk.CheckButton(_("Check for updates next launch"))
405         self.settings_box.pack_start(self.update_checkbox, True, True, 0)
406         if self.common.settings['check_for_updates']:
407             self.update_checkbox.set_active(True) 
408         else:
409             self.update_checkbox.set_active(False) 
410         self.update_checkbox.show()
411
412         # labels
413         if(self.common.settings['installed_version'][self.common.settings['preferred']]):
414             self.label1 = gtk.Label(_('Installed version:\n{0}').format(self.common.settings['installed_version'][self.common.settings['preferred']]))
415         else:
416             self.label1 = gtk.Label(_('Not installed'))
417         self.label1.set_line_wrap(True)
418         self.labels_box.pack_start(self.label1, True, True, 0)
419         self.label1.show()
420
421         if(self.common.settings['last_update_check_timestamp'] > 0):
422             self.label1 = gtk.Label(_('Last checked for updates:\n{0}').format(time.strftime("%B %d, %Y %I:%M %P", time.gmtime(self.common.settings['last_update_check_timestamp']))))
423         else:
424             self.label1 = gtk.Label(_('Never checked for updates'))
425         self.label1.set_line_wrap(True)
426         self.labels_box.pack_start(self.label1, True, True, 0)
427         self.label1.show()
428
429         # mirrors
430         self.mirrors_box = gtk.HBox(False, 10)
431         self.box.pack_start(self.mirrors_box, True, True, 0)
432         self.mirrors_box.show()
433
434         self.mirrors_label = gtk.Label(_('Mirror'))
435         self.mirrors_label.set_line_wrap(True)
436         self.mirrors_box.pack_start(self.mirrors_label, True, True, 0)
437         self.mirrors_label.show()
438
439         self.mirrors = gtk.combo_box_new_text()
440         for mirror in self.common.mirrors:
441             self.mirrors.append_text(mirror)
442         if self.common.settings['mirror'] in self.common.mirrors:
443             self.mirrors.set_active( self.common.mirrors.index(self.common.settings['mirror']) )
444         else:
445             self.preferred.set_active(0)
446         self.mirrors_box.pack_start(self.mirrors, True, True, 0)
447         self.mirrors.show()
448
449         # button box
450         self.button_box = gtk.HButtonBox()
451         self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
452         self.box.pack_start(self.button_box, True, True, 0)
453         self.button_box.show()
454
455         # save and launch button
456         save_launch_image = gtk.Image()
457         save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
458         self.save_launch_button = gtk.Button(_("Launch Tor Browser"))
459         self.save_launch_button.set_image(save_launch_image)
460         self.save_launch_button.connect("clicked", self.save_launch, None)
461         self.button_box.add(self.save_launch_button)
462         self.save_launch_button.show()
463
464         # save and exit button
465         save_exit_image = gtk.Image()
466         save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
467         self.save_exit_button = gtk.Button(_("Save & Exit"))
468         self.save_exit_button.set_image(save_exit_image)
469         self.save_exit_button.connect("clicked", self.save_exit, None)
470         self.button_box.add(self.save_exit_button)
471         self.save_exit_button.show()
472
473         # cancel button
474         cancel_image = gtk.Image()
475         cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
476         self.cancel_button = gtk.Button(_("Cancel"))
477         self.cancel_button.set_image(cancel_image)
478         self.cancel_button.connect("clicked", self.destroy, None)
479         self.button_box.add(self.cancel_button)
480         self.cancel_button.show()
481
482         # show the window
483         self.window.show()
484
485         # start gtk
486         gtk.main()
487
488     # save and launch
489     def save_launch(self, widget, data=None):
490         self.save()
491         p = subprocess.Popen([self.common.paths['tbl_bin']])
492         self.destroy(False)
493
494     # save and exit
495     def save_exit(self, widget, data=None):
496         self.save()
497         self.destroy(False)
498
499     # save settings
500     def save(self):
501         # figure out the selected preferred option
502         preferred = None
503         selected = self.preferred_options[self.preferred.get_active()]
504         for i in self.common.available_versions:
505             if self.common.available_versions[i] == selected:
506                 preferred = i
507         if preferred:
508             self.common.settings['preferred'] = preferred
509
510         # checkbox options
511         self.common.settings['update_over_tor'] = self.tor_update_checkbox.get_active()
512         self.common.settings['check_for_updates'] = self.update_checkbox.get_active()
513
514         # figure out the selected mirror
515         self.common.settings['mirror'] = self.common.mirrors[self.mirrors.get_active()]
516
517         # save them
518         self.common.save_settings()
519
520     # exit
521     def delete_event(self, widget, event, data=None):
522         return False
523     def destroy(self, widget, data=None):
524         gtk.main_quit()
525
526
527 class TBLLauncher:
528     def __init__(self, common):
529         print _('Starting launcher dialog')
530         self.common = common
531
532         # init launcher
533         self.set_gui(None, '', [])
534         self.launch_gui = True
535         self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
536
537         # is vidalia already running and we just need to open a new firefox?
538         if self.common.settings['installed_version']:
539             vidalia_pid = self.common.get_pid('./App/vidalia')
540             firefox_pid = self.common.get_pid(self.common.paths['tbb'][self.common.settings['preferred']]['firefox_bin'])
541
542             if vidalia_pid and not firefox_pid:
543                 print _('Vidalia is already open, but Firefox is closed. Launching new Firefox.')
544                 self.common.bring_window_to_front(vidalia_pid)
545                 subprocess.Popen([self.common.paths['tbb'][self.common.settings['preferred']]['firefox_bin'], '-no-remote', '-profile', self.common.paths['tbb'][self.common.settings['preferred']]['firefox_profile']])
546                 return
547             elif vidalia_pid and firefox_pid:
548                 print _('Vidalia and Firefox are already open, bringing them to focus')
549
550                 # bring firefox to front, then vidalia
551                 self.common.bring_window_to_front(firefox_pid)
552                 self.common.bring_window_to_front(vidalia_pid)
553                 return
554
555         # check for updates?
556         check_for_updates = False
557         if self.common.settings['check_for_updates']:
558             check_for_updates = True
559
560         if not check_for_updates:
561             # how long was it since the last update check?
562             # 86400 seconds = 24 hours
563             current_timestamp = int(time.time())
564             if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
565                 check_for_updates = True
566
567         if check_for_updates:
568             # check for update
569             print 'Checking for update'
570             self.set_gui('task', _("Checking for Tor Browser update."), 
571                 ['download_update_check', 
572                  'attempt_update'])
573         else:
574             # no need to check for update
575             print _('Checked for update within 24 hours, skipping')
576             self.start_launcher()
577
578         if self.launch_gui:
579             # set up the window
580             self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
581             self.window.set_title(_("Tor Browser"))
582             self.window.set_icon_from_file(self.common.paths['icon_file'])
583             self.window.set_position(gtk.WIN_POS_CENTER)
584             self.window.set_border_width(10)
585             self.window.connect("delete_event", self.delete_event)
586             self.window.connect("destroy", self.destroy)
587
588             # build the rest of the UI
589             self.build_ui()
590
591     # download or run TBB
592     def start_launcher(self):
593         # is TBB already installed?
594         start = self.common.paths['tbb'][self.common.settings['preferred']]['start']
595         if os.path.isfile(start) and os.access(start, os.X_OK):
596             if self.common.settings['installed_version'][self.common.settings['preferred']] == self.common.settings['latest_version'][self.common.settings['preferred']]:
597                 # current version of tbb is installed, launch it
598                 self.run(False)
599                 self.launch_gui = False
600             elif self.common.settings['installed_version'][self.common.settings['preferred']] < self.common.settings['latest_version'][self.common.settings['preferred']]:
601                 # there is a tbb upgrade available
602                 self.set_gui('task', _("Your Tor Browser is out of date."), 
603                     ['download_tarball_sig', 
604                      'download_tarball', 
605                      'verify', 
606                      'extract', 
607                      'run'])
608             else:
609                 # for some reason the installed tbb is newer than the current version?
610                 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
611
612         # not installed
613         else:
614             # are the tarball and sig already downloaded?
615             if os.path.isfile(self.common.paths['tarball_file']) and os.path.isfile(self.common.paths['tarball_sig_file']):
616                 # start the gui with verify
617                 self.set_gui('task', _("Installing Tor Browser."), 
618                     ['verify', 
619                      'extract', 
620                      'run'])
621
622             # first run
623             else:
624                 self.set_gui('task', _("Downloading and installing Tor Browser."), 
625                     ['download_tarball_sig', 
626                      'download_tarball', 
627                      'verify', 
628                      'extract', 
629                      'run'])
630    
631     # there are different GUIs that might appear, this sets which one we want
632     def set_gui(self, gui, message, tasks, autostart=True):
633         self.gui = gui
634         self.gui_message = message
635         self.gui_tasks = tasks
636         self.gui_task_i = 0
637         self.gui_autostart = autostart
638
639     # set all gtk variables to False
640     def clear_ui(self):
641         if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
642             self.box.destroy()
643         self.box = False
644
645         self.label = False
646         self.progressbar = False
647         self.button_box = False
648         self.start_button = False
649         self.exit_button = False
650
651     # build the application's UI
652     def build_ui(self):
653         self.clear_ui()
654
655         self.box = gtk.VBox(False, 20)
656         self.window.add(self.box)
657
658         if 'error' in self.gui:
659             # labels
660             self.label = gtk.Label( self.gui_message ) 
661             self.label.set_line_wrap(True)
662             self.box.pack_start(self.label, True, True, 0)
663             self.label.show()
664
665             # button box
666             self.button_box = gtk.HButtonBox()
667             self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
668             self.box.pack_start(self.button_box, True, True, 0)
669             self.button_box.show()
670
671             if self.gui != 'error':
672                 # yes button
673                 yes_image = gtk.Image()
674                 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
675                 self.yes_button = gtk.Button("Yes")
676                 self.yes_button.set_image(yes_image)
677                 if self.gui == 'error_try_stable':
678                     self.yes_button.connect("clicked", self.try_stable, None)
679                 elif self.gui == 'error_try_default_mirror':
680                     self.yes_button.connect("clicked", self.try_default_mirror, None)
681                 elif self.gui == 'error_try_tor':
682                     self.yes_button.connect("clicked", self.try_tor, None)
683                 self.button_box.add(self.yes_button)
684                 self.yes_button.show()
685
686             # exit button
687             exit_image = gtk.Image()
688             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
689             self.exit_button = gtk.Button("Exit")
690             self.exit_button.set_image(exit_image)
691             self.exit_button.connect("clicked", self.destroy, None)
692             self.button_box.add(self.exit_button)
693             self.exit_button.show()
694
695         elif self.gui == 'task':
696             # label
697             self.label = gtk.Label( self.gui_message ) 
698             self.label.set_line_wrap(True)
699             self.box.pack_start(self.label, True, True, 0)
700             self.label.show()
701             
702             # progress bar
703             self.progressbar = gtk.ProgressBar(adjustment=None)
704             self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
705             self.progressbar.set_pulse_step(0.01)
706             self.box.pack_start(self.progressbar, True, True, 0)
707
708             # button box
709             self.button_box = gtk.HButtonBox()
710             self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
711             self.box.pack_start(self.button_box, True, True, 0)
712             self.button_box.show()
713
714             # start button
715             start_image = gtk.Image()
716             start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
717             self.start_button = gtk.Button(_("Start"))
718             self.start_button.set_image(start_image)
719             self.start_button.connect("clicked", self.start, None)
720             self.button_box.add(self.start_button)
721             if not self.gui_autostart:
722               self.start_button.show()
723
724             # exit button
725             exit_image = gtk.Image()
726             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
727             self.exit_button = gtk.Button(_("Exit"))
728             self.exit_button.set_image(exit_image)
729             self.exit_button.connect("clicked", self.destroy, None)
730             self.button_box.add(self.exit_button)
731             self.exit_button.show()
732
733         self.box.show()
734         self.window.show()
735
736         if self.gui_autostart:
737             self.start(None)
738
739     # start button clicked, begin tasks
740     def start(self, widget, data=None):
741         # disable the start button
742         if self.start_button:
743             self.start_button.set_sensitive(False)
744
745         # start running tasks
746         self.run_task()
747       
748     # run the next task in the task list
749     def run_task(self):
750         self.refresh_gtk()
751
752         if self.gui_task_i >= len(self.gui_tasks):
753             self.destroy(False)
754             return
755
756         task = self.gui_tasks[self.gui_task_i]
757         
758         # get ready for the next task
759         self.gui_task_i += 1
760
761         if task == 'download_update_check':
762             print _('Downloading'), self.common.paths['update_check_url']
763             self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
764         
765         if task == 'attempt_update':
766             print _('Checking to see if update is needed')
767             self.attempt_update()
768
769         elif task == 'download_tarball':
770             print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
771             self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
772
773         elif task == 'download_tarball_sig':
774             print _('Downloading'), self.common.paths['tarball_sig_url'].format(self.common.settings['mirror'])
775             self.download('signature', self.common.paths['tarball_sig_url'], self.common.paths['tarball_sig_file'])
776
777         elif task == 'verify':
778             print _('Verifying signature')
779             self.verify()
780
781         elif task == 'extract':
782             print _('Extracting'), self.common.paths['tarball_filename']
783             self.extract()
784
785         elif task == 'run':
786             print _('Running'), self.common.paths['tbb'][self.common.settings['preferred']]['start']
787             self.run()
788         
789         elif task == 'start_over':
790             print _('Starting download over again')
791             self.start_over()
792
793     def response_received(self, response):
794         class FileDownloader(Protocol):
795             def __init__(self, common, file, total, progress, done_cb):
796                 self.file = file
797                 self.total = total
798                 self.so_far = 0
799                 self.progress = progress
800                 self.all_done = done_cb
801
802                 if response.code != 200:
803                     try_stable = False
804
805                     if response.code == 404:
806                         if common.settings['preferred'] == 'alpha' and common.language != 'en-US':
807                             try_stable = True
808                     
809                     if try_stable:
810                         raise TryStableException(_("It looks like the alpha version of Tor Browser Bundle isn't available for your language. Would you like to try the stable version instead?"))
811                     else:
812                         if common.settings['mirror'] != common.default_mirror:
813                             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']))
814                         else:
815                             raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
816
817             def dataReceived(self, bytes):
818                 self.file.write(bytes)
819                 self.so_far += len(bytes)
820                 percent = float(self.so_far) / float(self.total)
821                 self.progress.set_fraction(percent)
822                 amount = float(self.so_far)
823                 units = "bytes"
824                 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
825                     if amount > size:
826                         units = unit
827                         amount = amount / float(size)
828                         break
829
830                 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
831
832             def connectionLost(self, reason):
833                 print _('Finished receiving body:'), reason.getErrorMessage()
834                 self.all_done(reason)
835
836         dl = FileDownloader(self.common, self.file_download, response.length, self.progressbar, self.response_finished)
837         response.deliverBody(dl) 
838
839     def response_finished(self, msg):
840         if msg.check(ResponseDone):
841             self.file_download.close()
842             delattr(self, 'current_download_path')
843
844             # next task!
845             self.run_task()
846
847         else:
848             print "FINISHED", msg
849             ## FIXME handle errors
850
851     def download_error(self, f):
852         print _("Download error:"), f.value, type(f.value)
853
854         if isinstance(f.value, TryStableException):
855             f.trap(TryStableException)
856             self.set_gui('error_try_stable', str(f.value), [], False)
857         
858         elif isinstance(f.value, TryDefaultMirrorException):
859             f.trap(TryDefaultMirrorException)
860             self.set_gui('error_try_default_mirror', str(f.value), [], False)
861         
862         elif isinstance(f.value, DownloadErrorException):
863             f.trap(DownloadErrorException)
864             self.set_gui('error', str(f.value), [], False)
865         
866         elif isinstance(f.value, DNSLookupError):
867             f.trap(DNSLookupError)
868             if common.settings['mirror'] != common.default_mirror:
869                 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)
870             else:
871                 self.set_gui('error', str(f.value), [], False)
872
873         elif isinstance(f.value, ResponseFailed):
874             for reason in f.value.reasons:
875                 if isinstance(reason.value, OpenSSL.SSL.Error):
876                     # TODO: add the ability to report attack by posting bug to trac.torproject.org
877                     if not self.common.settings['update_over_tor']:
878                         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)
879                     else:
880                         self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
881
882         else:
883             self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
884         
885         self.build_ui()
886
887     def download(self, name, url, path):
888         # keep track of current download
889         self.current_download_path = path
890
891         # initialize the progress bar
892         mirror_url = url.format(self.common.settings['mirror'])
893         self.progressbar.set_fraction(0) 
894         self.progressbar.set_text(_('Downloading {0}').format(name))
895         self.progressbar.show()
896         self.refresh_gtk()
897         
898         # default mirror gets certificate pinning, only for requests that use the mirror
899         if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
900             agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
901         else:
902             agent = Agent(reactor)
903
904         # actually, agent needs to follow redirect
905         agent = RedirectAgent(agent)
906
907         # start the request
908         d = agent.request('GET', mirror_url,
909                           Headers({'User-Agent': ['torbrowser-launcher']}),
910                           None)
911         
912         self.file_download = open(path, 'w')
913         d.addCallback(self.response_received).addErrback(self.download_error)
914         
915         if not reactor.running:
916             reactor.run()
917
918     def try_stable(self, widget, data=None):
919         # change preferred to stable and relaunch TBL
920         self.common.settings['preferred'] = 'stable'
921         self.common.save_settings()
922         p = subprocess.Popen([self.common.paths['tbl_bin']])
923         self.destroy(False)
924
925     def try_default_mirror(self, widget, data=None):
926         # change preferred to stable and relaunch TBL
927         self.common.settings['mirror'] = self.common.default_mirror
928         self.common.save_settings()
929         p = subprocess.Popen([self.common.paths['tbl_bin']])
930         self.destroy(False)
931
932     def try_tor(self, widget, data=None):
933         # set update_over_tor to true and relaunch TBL
934         self.common.settings['update_over_tor'] = True
935         self.common.save_settings()
936         p = subprocess.Popen([self.common.paths['tbl_bin']])
937         self.destroy(False)
938
939     def attempt_update(self):
940         # load the update check file
941         try:
942             versions = json.load(open(self.common.paths['update_check_file']))
943             latest_stable = None
944             latest_alpha = None
945
946             # filter linux versions
947             valid_alphas = []
948             valid_stables = []
949             for version in versions:
950                 if str(version).find('-Linux') != -1:
951                     if version.find('alpha') != -1:
952                         valid_alphas.append(str(version))
953                     else:
954                         valid_stables.append(str(version))
955             valid_alphas.sort()
956             if len(valid_alphas):
957                 latest_alpha = valid_alphas.pop()
958             valid_stables.sort()
959             if len(valid_stables):
960                 latest_stable = valid_stables.pop()
961
962             if latest_stable or latest_alpha:
963                 if latest_stable:
964                     self.common.settings['latest_version']['stable'] = latest_stable[:-len('-Linux')]
965                 if latest_alpha:
966                     self.common.settings['latest_version']['alpha'] = latest_alpha[:-len('-Linux')]
967                 self.common.settings['last_update_check_timestamp'] = int(time.time())
968                 self.common.settings['check_for_updates'] = False
969                 self.common.save_settings()
970                 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
971                 self.start_launcher()
972
973             else:
974                 # failed to find the latest version
975                 self.set_gui('error', _("Error checking for updates."), [], False)
976         
977         except:
978             # not a valid JSON object
979             self.set_gui('error', _("Error checking for updates."), [], False)
980
981         # now start over
982         self.clear_ui()
983         self.build_ui()
984
985     def verify(self):
986         # initialize the progress bar
987         self.progressbar.set_fraction(0) 
988         self.progressbar.set_text(_('Verifying Signature'))
989         self.progressbar.show()
990
991         p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['tarball_sig_file']])
992         self.pulse_until_process_exits(p)
993         
994         if p.returncode == 0:
995             self.run_task()
996         else:
997             # TODO: add the ability to report attack by posting bug to trac.torproject.org
998             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)
999             self.clear_ui()
1000             self.build_ui()
1001
1002             if not reactor.running:
1003                 reactor.run()
1004
1005     def extract(self):
1006         # initialize the progress bar
1007         self.progressbar.set_fraction(0) 
1008         self.progressbar.set_text(_('Installing'))
1009         self.progressbar.show()
1010         self.refresh_gtk()
1011
1012         # make sure this file is a tarfile
1013         if tarfile.is_tarfile(self.common.paths['tarball_file']):
1014           tf = tarfile.open(self.common.paths['tarball_file'])
1015           tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1016         else:
1017             self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}"), ['start_over'], False)
1018             self.clear_ui()
1019             self.build_ui()
1020
1021         # installation is finished, so save installed_version
1022         self.common.settings['installed_version'] = self.common.settings['latest_version']
1023         self.common.save_settings()
1024
1025         self.run_task()
1026
1027     def run(self, run_next_task = True):
1028         subprocess.Popen([self.common.paths['tbb'][self.common.settings['preferred']]['start']])
1029         if run_next_task:
1030             self.run_task()
1031
1032     # make the progress bar pulse until process p (a Popen object) finishes
1033     def pulse_until_process_exits(self, p):
1034         while p.poll() == None:
1035             time.sleep(0.01)
1036             self.progressbar.pulse()
1037             self.refresh_gtk()
1038
1039     # start over and download TBB again
1040     def start_over(self):
1041         self.label.set_text(_("Downloading Tor Browser Bundle over again."))
1042         self.gui_tasks = ['download_tarball', 'download_tarball_sig', 'verify', 'extract', 'run']
1043         self.gui_task_i = 0
1044         self.start(None)
1045    
1046     # refresh gtk
1047     def refresh_gtk(self):
1048         while gtk.events_pending():
1049             gtk.main_iteration(False)
1050
1051     # exit
1052     def delete_event(self, widget, event, data=None):
1053         return False
1054     def destroy(self, widget, data=None):
1055         if hasattr(self, 'file_download'):
1056             self.file_download.close()
1057         if hasattr(self, 'current_download_path'):
1058             os.remove(self.current_download_path)
1059             delattr(self, 'current_download_path')
1060         if reactor.running:
1061             reactor.stop()
1062
1063 if __name__ == "__main__":
1064     tor_browser_launcher_version = '0.0.2'
1065
1066     print _('Tor Browser Launcher')
1067     print _('By Micah Lee, licensed under GPLv3')
1068     print _('version {0}').format(tor_browser_launcher_version)
1069     print 'https://github.com/micahflee/torbrowser-launcher'
1070
1071     common = TBLCommon(tor_browser_launcher_version)
1072
1073     # is torbrowser-launcher already running?
1074     tbl_pid = common.get_pid(common.paths['tbl_bin'], True)
1075     if tbl_pid:
1076         print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
1077         common.bring_window_to_front(tbl_pid)
1078         sys.exit()
1079
1080     if '-settings' in sys.argv:
1081         # settings mode
1082         app = TBLSettings(common)
1083
1084     else:
1085         # launcher mode
1086         app = TBLLauncher(common)
1087