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