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