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