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