]> git.lizzy.rs Git - torbrowser-launcher.git/blob - torbrowser-launcher
Merge pull request #46 from pde/master
[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             # next task!
843             self.run_task()
844
845         else:
846             print "FINISHED", msg
847             ## FIXME handle errors
848
849     def download_error(self, f):
850         print _("Download error:"), f.value, type(f.value)
851
852         if isinstance(f.value, TryStableException):
853             f.trap(TryStableException)
854             self.set_gui('error_try_stable', str(f.value), [], False)
855         
856         elif isinstance(f.value, TryDefaultMirrorException):
857             f.trap(TryDefaultMirrorException)
858             self.set_gui('error_try_default_mirror', str(f.value), [], False)
859         
860         elif isinstance(f.value, DownloadErrorException):
861             f.trap(DownloadErrorException)
862             self.set_gui('error', str(f.value), [], False)
863         
864         elif isinstance(f.value, DNSLookupError):
865             f.trap(DNSLookupError)
866             if common.settings['mirror'] != common.default_mirror:
867                 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)
868             else:
869                 self.set_gui('error', str(f.value), [], False)
870
871         elif isinstance(f.value, ResponseFailed):
872             for reason in f.value.reasons:
873                 if isinstance(reason.value, OpenSSL.SSL.Error):
874                     # TODO: add the ability to report attack by posting bug to trac.torproject.org
875                     if not self.common.settings['update_over_tor']:
876                         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)
877                     else:
878                         self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
879
880         else:
881             self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
882         
883         self.build_ui()
884
885     def download(self, name, url, path):
886         # initialize the progress bar
887         mirror_url = url.format(self.common.settings['mirror'])
888         self.progressbar.set_fraction(0) 
889         self.progressbar.set_text(_('Downloading {0}').format(name))
890         self.progressbar.show()
891         self.refresh_gtk()
892         
893         # default mirror gets certificate pinning, only for requests that use the mirror
894         if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
895             agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
896         else:
897             agent = Agent(reactor)
898
899         # actually, agent needs to follow redirect
900         agent = RedirectAgent(agent)
901
902         # start the request
903         d = agent.request('GET', mirror_url,
904                           Headers({'User-Agent': ['torbrowser-launcher']}),
905                           None)
906         
907         self.file_download = open(path, 'w')
908         d.addCallback(self.response_received).addErrback(self.download_error)
909         
910         if not reactor.running:
911             reactor.run()
912
913     def try_stable(self, widget, data=None):
914         # change preferred to stable and relaunch TBL
915         self.common.settings['preferred'] = 'stable'
916         self.common.save_settings()
917         p = subprocess.Popen([self.common.paths['tbl_bin']])
918         self.destroy(False)
919
920     def try_default_mirror(self, widget, data=None):
921         # change preferred to stable and relaunch TBL
922         self.common.settings['mirror'] = self.common.default_mirror
923         self.common.save_settings()
924         p = subprocess.Popen([self.common.paths['tbl_bin']])
925         self.destroy(False)
926
927     def try_tor(self, widget, data=None):
928         # set update_over_tor to true and relaunch TBL
929         self.common.settings['update_over_tor'] = True
930         self.common.save_settings()
931         p = subprocess.Popen([self.common.paths['tbl_bin']])
932         self.destroy(False)
933
934     def attempt_update(self):
935         # load the update check file
936         try:
937             versions = json.load(open(self.common.paths['update_check_file']))
938             latest_stable = None
939             latest_alpha = None
940
941             # filter linux versions
942             valid_versions = []
943             for version in versions:
944                 if str(version).find('-Linux') != -1:
945                     valid_versions.append(str(version))
946             # find alpha
947             for version in valid_versions:
948                 if version.find('alpha') != -1:
949                     latest_alpha = version
950                     valid_versions.remove(latest_alpha)
951             # find stable (whatever is left after alpha)
952             if len(valid_versions):
953                 latest_stable = valid_versions[0]
954
955             if latest_stable or latest_alpha:
956                 if latest_stable:
957                     self.common.settings['latest_version']['stable'] = latest_stable[:-len('-Linux')]
958                 if latest_alpha:
959                     self.common.settings['latest_version']['alpha'] = latest_alpha[:-len('-Linux')]
960                 self.common.settings['last_update_check_timestamp'] = int(time.time())
961                 self.common.settings['check_for_updates'] = False
962                 self.common.save_settings()
963                 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
964                 self.start_launcher()
965
966             else:
967                 # failed to find the latest version
968                 self.set_gui('error', _("Error checking for updates."), [], False)
969         
970         except:
971             # not a valid JSON object
972             self.set_gui('error', _("Error checking for updates."), [], False)
973
974         # now start over
975         self.clear_ui()
976         self.build_ui()
977
978     def verify(self):
979         # initialize the progress bar
980         self.progressbar.set_fraction(0) 
981         self.progressbar.set_text(_('Verifying Signature'))
982         self.progressbar.show()
983
984         p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['tarball_sig_file']])
985         self.pulse_until_process_exits(p)
986         
987         if p.returncode == 0:
988             self.run_task()
989         else:
990             # TODO: add the ability to report attack by posting bug to trac.torproject.org
991             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)
992             self.clear_ui()
993             self.build_ui()
994
995             if not reactor.running:
996                 reactor.run()
997
998     def extract(self):
999         # initialize the progress bar
1000         self.progressbar.set_fraction(0) 
1001         self.progressbar.set_text(_('Installing'))
1002         self.progressbar.show()
1003         self.refresh_gtk()
1004
1005         # make sure this file is a tarfile
1006         if tarfile.is_tarfile(self.common.paths['tarball_file']):
1007           tf = tarfile.open(self.common.paths['tarball_file'])
1008           tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1009         else:
1010             self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}"), ['start_over'], False)
1011             self.clear_ui()
1012             self.build_ui()
1013
1014         # installation is finished, so save installed_version
1015         self.common.settings['installed_version'] = self.common.settings['latest_version']
1016         self.common.save_settings()
1017
1018         self.run_task()
1019
1020     def run(self, run_next_task = True):
1021         subprocess.Popen([self.common.paths['tbb'][self.common.settings['preferred']]['start']])
1022         if run_next_task:
1023             self.run_task()
1024
1025     # make the progress bar pulse until process p (a Popen object) finishes
1026     def pulse_until_process_exits(self, p):
1027         while p.poll() == None:
1028             time.sleep(0.01)
1029             self.progressbar.pulse()
1030             self.refresh_gtk()
1031
1032     # start over and download TBB again
1033     def start_over(self):
1034         self.label.set_text(_("Downloading Tor Browser Bundle over again."))
1035         self.gui_tasks = ['download_tarball', 'download_tarball_sig', 'verify', 'extract', 'run']
1036         self.gui_task_i = 0
1037         self.start(None)
1038    
1039     # refresh gtk
1040     def refresh_gtk(self):
1041         while gtk.events_pending():
1042             gtk.main_iteration(False)
1043
1044     # exit
1045     def delete_event(self, widget, event, data=None):
1046         return False
1047     def destroy(self, widget, data=None):
1048         if hasattr(self, 'file_download'):
1049             self.file_download.close()
1050         if reactor.running:
1051             reactor.stop()
1052
1053 if __name__ == "__main__":
1054     tor_browser_launcher_version = '0.0.2'
1055
1056     print _('Tor Browser Launcher')
1057     print _('By Micah Lee, licensed under GPLv3')
1058     print _('version {0}').format(tor_browser_launcher_version)
1059     print 'https://github.com/micahflee/torbrowser-launcher'
1060
1061     common = TBLCommon(tor_browser_launcher_version)
1062
1063     # is torbrowser-launcher already running?
1064     tbl_pid = common.get_pid(common.paths['tbl_bin'], True)
1065     if tbl_pid:
1066         print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
1067         common.bring_window_to_front(tbl_pid)
1068         sys.exit()
1069
1070     if '-settings' in sys.argv:
1071         # settings mode
1072         app = TBLSettings(common)
1073
1074     else:
1075         # launcher mode
1076         app = TBLLauncher(common)
1077