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