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