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