]> git.lizzy.rs Git - torbrowser-launcher.git/blob - torbrowser-launcher
more logic to to make new settings worth (#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.pref_ver_box = gtk.HBox(False, 10)
363         self.settings_box.pack_start(self.pref_ver_box, True, True, 0)
364         self.pref_ver_box.show()
365
366         self.pref_ver_label = gtk.Label(_('I prefer'))
367         self.pref_ver_label.set_line_wrap(True)
368         self.pref_ver_box.pack_start(self.pref_ver_label, True, True, 0)
369         self.pref_ver_label.show()
370
371         options = []
372         for i in self.common.available_versions:
373             options.append(self.common.available_versions[i])
374         options.sort()
375
376         self.pref_ver = gtk.combo_box_new_text()
377         for option in options:
378             self.pref_ver.append_text(option)
379         self.pref_ver.set_active(0)
380         self.pref_ver_box.pack_start(self.pref_ver, True, True, 0)
381         self.pref_ver.show()
382
383         # download over tor
384         self.tor_update_checkbox = gtk.CheckButton(_("Download updates over Tor (recommended)"))
385         self.settings_box.pack_start(self.tor_update_checkbox, True, True, 0)
386         self.tor_update_checkbox.show()
387
388         # check for updates
389         self.update_checkbox = gtk.CheckButton(_("Check for updates next launch"))
390         self.settings_box.pack_start(self.update_checkbox, True, True, 0)
391         self.update_checkbox.show()
392
393         # labels
394         if(self.common.settings['installed_version']):
395             self.label1 = gtk.Label(_('Installed version:\n{0}').format(self.common.settings['installed_version']))
396         else:
397             self.label1 = gtk.Label(_('Not installed'))
398         self.label1.set_line_wrap(True)
399         self.labels_box.pack_start(self.label1, True, True, 0)
400         self.label1.show()
401
402         if(self.common.settings['last_update_check_timestamp'] > 0):
403             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']))))
404         else:
405             self.label1 = gtk.Label(_('Never checked for updates'))
406         self.label1.set_line_wrap(True)
407         self.labels_box.pack_start(self.label1, True, True, 0)
408         self.label1.show()
409
410         # button box
411         self.button_box = gtk.HButtonBox()
412         self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
413         self.box.pack_start(self.button_box, True, True, 0)
414         self.button_box.show()
415
416         # save and launch button
417         save_launch_image = gtk.Image()
418         save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
419         self.save_launch_button = gtk.Button(_("Launch Tor Browser"))
420         self.save_launch_button.set_image(save_launch_image)
421         self.save_launch_button.connect("clicked", self.save_launch, None)
422         self.button_box.add(self.save_launch_button)
423         self.save_launch_button.show()
424
425         # save and exit button
426         save_exit_image = gtk.Image()
427         save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
428         self.save_exit_button = gtk.Button(_("Save & Exit"))
429         self.save_exit_button.set_image(save_exit_image)
430         self.save_exit_button.connect("clicked", self.save_exit, None)
431         self.button_box.add(self.save_exit_button)
432         self.save_exit_button.show()
433
434         # cancel button
435         cancel_image = gtk.Image()
436         cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
437         self.cancel_button = gtk.Button(_("Cancel"))
438         self.cancel_button.set_image(cancel_image)
439         self.cancel_button.connect("clicked", self.destroy, None)
440         self.button_box.add(self.cancel_button)
441         self.cancel_button.show()
442
443         # show the window
444         self.window.show()
445
446         # start gtk
447         gtk.main()
448
449     # save and launch
450     def save_launch(self, widget, data=None):
451         self.save()
452         p = subprocess.Popen([self.common.paths['tbl_bin']])
453         self.destroy(False)
454
455     # save and exit
456     def save_exit(self, widget, data=None):
457         self.save()
458         self.destroy(False)
459
460     # save settings
461     def save(self):
462         pass
463
464     # exit
465     def delete_event(self, widget, event, data=None):
466         return False
467     def destroy(self, widget, data=None):
468         gtk.main_quit()
469
470
471 class TBLLauncher:
472     def __init__(self, common):
473         print _('Starting launcher dialog')
474         self.common = common
475
476         # init launcher
477         self.set_gui(None, '', [])
478         self.launch_gui = True
479         self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
480
481         # is vidalia already running and we just need to open a new firefox?
482         if self.common.settings['installed_version']:
483             vidalia_pid = self.common.get_pid('./App/vidalia')
484             firefox_pid = self.common.get_pid(self.common.paths['tbb'][self.common.settings['preferred']]['firefox_bin'])
485
486             if vidalia_pid and not firefox_pid:
487                 print _('Vidalia is already open, but Firefox is closed. Launching new Firefox.')
488                 self.common.bring_window_to_front(vidalia_pid)
489                 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']])
490                 return
491             elif vidalia_pid and firefox_pid:
492                 print _('Vidalia and Firefox are already open, bringing them to focus')
493
494                 # bring firefox to front, then vidalia
495                 self.common.bring_window_to_front(firefox_pid)
496                 self.common.bring_window_to_front(vidalia_pid)
497                 return
498
499         # check for updates?
500         check_for_updates = False
501         if self.common.settings['check_for_updates']:
502             check_for_updates = True
503
504         if not check_for_updates:
505             # how long was it since the last update check?
506             # 86400 seconds = 24 hours
507             current_timestamp = int(time.time())
508             if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
509                 check_for_updates = True
510
511         if check_for_updates:
512             # check for update
513             print 'Checking for update'
514             self.set_gui('task', _("Checking for Tor Browser update."), 
515                 ['download_update_check', 
516                  'attempt_update'])
517         else:
518             # no need to check for update
519             print _('Checked for update within 24 hours, skipping')
520             self.start_launcher()
521
522         if self.launch_gui:
523             # set up the window
524             self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
525             self.window.set_title(_("Tor Browser"))
526             self.window.set_icon_from_file(self.common.paths['icon_file'])
527             self.window.set_position(gtk.WIN_POS_CENTER)
528             self.window.set_border_width(10)
529             self.window.connect("delete_event", self.delete_event)
530             self.window.connect("destroy", self.destroy)
531
532             # build the rest of the UI
533             self.build_ui()
534
535     # download or run TBB
536     def start_launcher(self):
537       # is TBB already installed?
538       start = self.common.paths['tbb'][self.common.settings['preferred']]['start']
539       if os.path.isfile(start) and os.access(start, os.X_OK):
540         if self.common.settings['installed_version'][self.common.settings['preferred']] == self.common.settings['latest_version'][self.common.settings['preferred']]:
541           # current version of tbb is installed, launch it
542           self.run(False)
543           self.launch_gui = False
544         elif self.common.settings['installed_version'][self.common.settings['preferred']] < self.common.settings['latest_version'][self.common.settings['preferred']]:
545           # there is a tbb upgrade available
546           self.set_gui('task', _("Your Tor Browser is out of date."), 
547             ['download_tarball', 
548              'download_tarball_sig', 
549              'verify', 
550              'extract', 
551              'run'])
552         else:
553           # for some reason the installed tbb is newer than the current version?
554           self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
555
556       # not installed
557       else:
558           # are the tarball and sig already downloaded?
559           if os.path.isfile(self.common.paths['tarball_file']) and os.path.isfile(self.common.paths['tarball_sig_file']):
560               # start the gui with verify
561               self.set_gui('task', _("Installing Tor Browser."), 
562                   ['verify', 
563                    'extract', 
564                    'run'])
565
566           # first run
567           else:
568               self.set_gui('task', _("Downloading and installing Tor Browser."), 
569                   ['download_tarball', 
570                    'download_tarball_sig', 
571                    'verify', 
572                    'extract', 
573                    'run'])
574    
575     # there are different GUIs that might appear, this sets which one we want
576     def set_gui(self, gui, message, tasks, autostart=True):
577         self.gui = gui
578         self.gui_message = message
579         self.gui_tasks = tasks
580         self.gui_task_i = 0
581         self.gui_autostart = autostart
582
583     # set all gtk variables to False
584     def clear_ui(self):
585         if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
586             self.box.destroy()
587         self.box = False
588
589         self.label = False
590         self.progressbar = False
591         self.button_box = False
592         self.start_button = False
593         self.exit_button = False
594
595     # build the application's UI
596     def build_ui(self):
597         self.clear_ui()
598
599         self.box = gtk.VBox(False, 20)
600         self.window.add(self.box)
601
602         if self.gui == 'error':
603             # labels
604             self.label = gtk.Label( self.gui_message ) 
605             self.label.set_line_wrap(True)
606             self.box.pack_start(self.label, True, True, 0)
607             self.label.show()
608
609             # exit button
610             exit_image = gtk.Image()
611             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
612             self.exit_button = gtk.Button("Exit")
613             self.exit_button.set_image(exit_image)
614             self.exit_button.connect("clicked", self.destroy, None)
615             self.box.add(self.exit_button)
616             self.exit_button.show()
617
618         elif self.gui == 'task':
619             # label
620             self.label = gtk.Label( self.gui_message ) 
621             self.label.set_line_wrap(True)
622             self.box.pack_start(self.label, True, True, 0)
623             self.label.show()
624             
625             # progress bar
626             self.progressbar = gtk.ProgressBar(adjustment=None)
627             self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
628             self.progressbar.set_pulse_step(0.01)
629             self.box.pack_start(self.progressbar, True, True, 0)
630
631             # button box
632             self.button_box = gtk.HButtonBox()
633             self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
634             self.box.pack_start(self.button_box, True, True, 0)
635             self.button_box.show()
636
637             # start button
638             start_image = gtk.Image()
639             start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
640             self.start_button = gtk.Button(_("Start"))
641             self.start_button.set_image(start_image)
642             self.start_button.connect("clicked", self.start, None)
643             self.button_box.add(self.start_button)
644             if not self.gui_autostart:
645               self.start_button.show()
646
647             # exit button
648             exit_image = gtk.Image()
649             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
650             self.exit_button = gtk.Button(_("Exit"))
651             self.exit_button.set_image(exit_image)
652             self.exit_button.connect("clicked", self.destroy, None)
653             self.button_box.add(self.exit_button)
654             self.exit_button.show()
655
656         self.box.show()
657         self.window.show()
658
659         if self.gui_autostart:
660             self.start(None)
661
662     # start button clicked, begin tasks
663     def start(self, widget, data=None):
664         # disable the start button
665         if self.start_button:
666             self.start_button.set_sensitive(False)
667
668         # start running tasks
669         self.run_task()
670       
671     # run the next task in the task list
672     def run_task(self):
673         self.refresh_gtk()
674
675         if self.gui_task_i >= len(self.gui_tasks):
676             self.destroy(False)
677             return
678
679         task = self.gui_tasks[self.gui_task_i]
680         
681         # get ready for the next task
682         self.gui_task_i += 1
683
684         if task == 'download_update_check':
685             print _('Downloading'), self.common.paths['update_check_url']
686             self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
687         
688         if task == 'attempt_update':
689             print _('Checking to see if update it needed')
690             self.attempt_update()
691
692         elif task == 'download_tarball':
693             print _('Downloading'), self.common.paths['tarball_url']
694             self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
695
696         elif task == 'download_tarball_sig':
697             print _('Downloading'), self.common.paths['tarball_sig_url']
698             self.download('signature', self.common.paths['tarball_sig_url'], self.common.paths['tarball_sig_file'])
699
700         elif task == 'verify':
701             print _('Verifying signature')
702             self.verify()
703
704         elif task == 'extract':
705             print _('Extracting'), self.common.paths['tarball_filename']
706             self.extract()
707
708         elif task == 'run':
709             print _('Running'), self.common.paths['tbb'][self.common.settings['preferred']]['start']
710             self.run()
711         
712         elif task == 'start_over':
713             print _('Starting download over again')
714             self.start_over()
715
716     def response_received(self, response):
717         class FileDownloader(Protocol):
718             def __init__(self, file, total, progress, done_cb):
719                 self.file = file
720                 self.total = total
721                 self.so_far = 0
722                 self.progress = progress
723                 self.all_done = done_cb
724
725             def dataReceived(self, bytes):
726                 self.file.write(bytes)
727                 self.so_far += len(bytes)
728                 percent = float(self.so_far) / float(self.total)
729                 self.progress.set_fraction(percent)
730                 amount = float(self.so_far)
731                 units = "bytes"
732                 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
733                     if amount > size:
734                         units = unit
735                         amount = amount / float(size)
736                         break
737
738                 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
739
740             def connectionLost(self, reason):
741                 print _('Finished receiving body:'), reason.getErrorMessage()
742                 self.all_done(reason)
743
744         dl = FileDownloader(self.file_download, response.length, self.progressbar, self.response_finished)
745         response.deliverBody(dl)
746
747     def response_finished(self, msg):
748         if msg.check(ResponseDone):
749             self.file_download.close()
750             # next task!
751             self.run_task()
752
753         else:
754             print "FINISHED", msg
755             ## FIXME handle errors
756
757     def download_error(self, f):
758         print _("Download error"), f
759         self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
760         self.clear_ui()
761         self.build_ui()
762
763     def download(self, name, url, path):
764         # initialize the progress bar
765         self.progressbar.set_fraction(0) 
766         self.progressbar.set_text(_('Downloading {0}').format(name))
767         self.progressbar.show()
768         self.refresh_gtk()
769
770         agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
771         d = agent.request('GET', url,
772                           Headers({'User-Agent': ['torbrowser-launcher']}),
773                           None)
774
775         self.file_download = open(path, 'w')
776         d.addCallback(self.response_received).addErrback(self.download_error)
777         
778         if not reactor.running:
779             reactor.run()
780
781     def attempt_update(self):
782         # load the update check file
783         try:
784             versions = json.load(open(self.common.paths['update_check_file']))
785             latest_stable = None
786             latest_alpha = None
787             latest_obs = None
788
789             # filter linux versions
790             valid_versions = []
791             for version in versions:
792                 if str(version).find('-Linux') != -1:
793                     valid_versions.append(str(version))
794             # find alpha
795             for version in valid_versions:
796                 if version.find('alpha') != -1:
797                     latest_alpha = valid_versions
798                     valid_versions.remove(latest_alpha)
799             # find stable (whatever is left after alpha)
800             if len(valid_versions):
801                 latest_stable = valid_versions[0]
802
803             if latest_stable or latest_alpha or latest_obs:
804                 if latest_stable:
805                     self.common.settings['latest_version']['stable'] = latest_stable[:-len('-Linux')]
806                 if latest_alpha:
807                     self.common.settings['latest_version']['alpha'] = latest_stable[:-len('-Linux')]
808                 if latest_obs:
809                     self.common.settings['latest_version']['obs'] = latest_stable[:-len('-Linux')]
810                 self.common.settings['last_update_check_timestamp'] = int(time.time())
811                 self.common.settings['check_for_updates'] = False
812                 self.common.save_settings()
813                 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
814                 self.start_launcher()
815
816             else:
817                 # failed to find the latest version
818                 self.set_gui('error', _("Error checking for updates."), [], False)
819         
820         except:
821             # not a valid JSON object
822             self.set_gui('error', _("Error checking for updates."), [], False)
823
824         # now start over
825         self.clear_ui()
826         self.build_ui()
827
828     def verify(self):
829         # initialize the progress bar
830         self.progressbar.set_fraction(0) 
831         self.progressbar.set_text(_('Verifying Signature'))
832         self.progressbar.show()
833
834         p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['tarball_sig_file']])
835         self.pulse_until_process_exits(p)
836         
837         if p.returncode == 0:
838             self.run_task()
839         else:
840             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)
841             self.clear_ui()
842             self.build_ui()
843
844             if not reactor.running:
845                 reactor.run()
846
847     def extract(self):
848         # initialize the progress bar
849         self.progressbar.set_fraction(0) 
850         self.progressbar.set_text(_('Installing'))
851         self.progressbar.show()
852         self.refresh_gtk()
853
854         # make sure this file is a tarfile
855         if tarfile.is_tarfile(self.common.paths['tarball_file']):
856           tf = tarfile.open(self.common.paths['tarball_file'])
857           tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
858         else:
859             self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}"), ['start_over'], False)
860             self.clear_ui()
861             self.build_ui()
862
863         # installation is finished, so save installed_version
864         self.common.settings['installed_version'] = self.common.settings['latest_version']
865         self.common.save_settings()
866
867         self.run_task()
868
869     def run(self, run_next_task = True):
870         subprocess.Popen([self.common.paths['tbb'][self.common.settings['preferred']]['start']])
871         if run_next_task:
872             self.run_task()
873
874     # make the progress bar pulse until process p (a Popen object) finishes
875     def pulse_until_process_exits(self, p):
876         while p.poll() == None:
877             time.sleep(0.01)
878             self.progressbar.pulse()
879             self.refresh_gtk()
880
881     # start over and download TBB again
882     def start_over(self):
883         self.label.set_text(_("Downloading Tor Browser Bundle over again."))
884         self.gui_tasks = ['download_tarball', 'download_tarball_sig', 'verify', 'extract', 'run']
885         self.gui_task_i = 0
886         self.start(None)
887    
888     # refresh gtk
889     def refresh_gtk(self):
890         while gtk.events_pending():
891             gtk.main_iteration(False)
892
893     # exit
894     def delete_event(self, widget, event, data=None):
895         return False
896     def destroy(self, widget, data=None):
897         if hasattr(self, 'file_download'):
898             self.file_download.close()
899         if reactor.running:
900             reactor.stop()
901
902 if __name__ == "__main__":
903     tor_browser_launcher_version = '0.0.2'
904
905     print _('Tor Browser Launcher')
906     print _('By Micah Lee, licensed under GPLv3')
907     print _('version {0}').format(tor_browser_launcher_version)
908     print 'https://github.com/micahflee/torbrowser-launcher'
909
910     common = TBLCommon(tor_browser_launcher_version)
911
912     # is torbrowser-launcher already running?
913     tbl_pid = common.get_pid(common.paths['tbl_bin'], True)
914     if tbl_pid:
915         print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
916         common.bring_window_to_front(tbl_pid)
917         sys.exit()
918
919     if '-settings' in sys.argv:
920         # settings mode
921         app = TBLSettings(common)
922
923     else:
924         # launcher mode
925         app = TBLLauncher(common)
926