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