]> git.lizzy.rs Git - torbrowser-launcher.git/blob - torbrowser-launcher
launches Tor Browser as a background process again to make the TBL window close ...
[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-2014 Micah Lee <micah@micahflee.com>
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 os
31 SHARE = os.getenv('TBL_SHARE', '/usr/share')
32 import sys
33 import platform
34
35 import gettext
36 gettext.install('torbrowser-launcher', os.path.join(SHARE, 'torbrowser-launcher/locale'))
37
38 from twisted.internet import gtk2reactor
39 gtk2reactor.install()
40 from twisted.internet import reactor
41
42 import pygtk
43 pygtk.require('2.0')
44 import gtk
45
46 import subprocess, locale, time, pickle, json, tarfile, psutil, hashlib, lzma
47
48 from twisted.web.client import Agent, RedirectAgent, ResponseDone, ResponseFailed
49 from twisted.web.http_headers import Headers
50 from twisted.internet.protocol import Protocol
51 from twisted.internet.ssl import ClientContextFactory
52 from twisted.internet.error import DNSLookupError
53
54 import OpenSSL
55
56
57 class TryStableException(Exception):
58     pass
59
60
61 class TryDefaultMirrorException(Exception):
62     pass
63
64
65 class DownloadErrorException(Exception):
66     pass
67
68
69 class VerifyTorProjectCert(ClientContextFactory):
70
71     def __init__(self, torproject_pem):
72         self.torproject_ca = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, open(torproject_pem, 'r').read())
73
74     def getContext(self, host, port):
75         ctx = ClientContextFactory.getContext(self)
76         ctx.set_verify_depth(0)
77         ctx.set_verify(OpenSSL.SSL.VERIFY_PEER | OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname)
78         return ctx
79
80     def verifyHostname(self, connection, cert, errno, depth, preverifyOK):
81         return cert.digest('sha256') == self.torproject_ca.digest('sha256')
82
83
84 class TBLCommon:
85
86     def __init__(self, tbl_version):
87         print _('Initializing Tor Browser Launcher')
88         self.tbl_version = tbl_version
89
90         # initialize the app
91         self.default_mirror = 'https://www.torproject.org/dist/'
92         self.discover_arch_lang()
93         self.build_paths()
94         self.mkdir(self.paths['data_dir'])
95         self.load_mirrors()
96         self.load_settings()
97         self.mkdir(self.paths['download_dir'])
98         self.mkdir(self.paths['tbb']['dir'])
99         self.init_gnupg()
100
101         # allow buttons to have icons
102         try:
103             gtk_settings = gtk.settings_get_default()
104             gtk_settings.props.gtk_button_images = True
105         except:
106             pass
107
108     # discover the architecture and language
109     def discover_arch_lang(self):
110         # figure out the architecture
111         self.architecture = 'x86_64' if '64' in platform.architecture()[0] else 'i686'
112
113         # figure out the language
114         available_languages = ['en-US', 'ar', 'de', 'es-ES', 'fa', 'fr', 'it', 'ko', 'nl', 'pl', 'pt-PT', 'ru', 'vi', 'zh-CN']
115         default_locale = locale.getdefaultlocale()[0]
116         if default_locale is None:
117             self.language = 'en-US'
118         else:
119             self.language = default_locale.replace('_', '-')
120             if self.language not in available_languages:
121                 self.language = self.language.split('-')[0]
122                 if self.language not in available_languages:
123                     for l in available_languages:
124                         if l[0:2] == self.language:
125                             self.language = l
126             # if language isn't available, default to english
127             if self.language not in available_languages:
128                 self.language = 'en-US'
129
130     # build all relevant paths
131     def build_paths(self, tbb_version=None):
132         homedir = os.getenv('HOME')
133         if not homedir:
134             homedir = '/tmp/.torbrowser-'+os.getenv('USER')
135             if not os.path.exists(homedir):
136                 try:
137                     os.mkdir(homedir, 0700)
138                 except:
139                     self.set_gui('error', _("Error creating {0}").format(homedir), [], False)
140         if not os.access(homedir, os.W_OK):
141             self.set_gui('error', _("{0} is not writable").format(homedir), [], False)
142
143         tbb_data = '%s/.torbrowser' % homedir
144
145         if tbb_version:
146             # tarball filename
147             if self.architecture == 'x86_64':
148                 arch = 'linux64'
149             else:
150                 arch = 'linux32'
151             tarball_filename = 'tor-browser-'+arch+'-'+tbb_version+'_'+self.language+'.tar.xz'
152
153             # tarball
154             self.paths['tarball_url'] = '{0}torbrowser/'+tbb_version+'/'+tarball_filename
155             self.paths['tarball_file'] = tbb_data+'/download/'+tarball_filename
156             self.paths['tarball_filename'] = tarball_filename
157
158             # sig
159             self.paths['sha256_file'] = tbb_data+'/download/sha256sums.txt'
160             self.paths['sha256_sig_file'] = tbb_data+'/download/sha256sums.txt.asc'
161             self.paths['sha256_url'] = '{0}torbrowser/'+tbb_version+'/sha256sums.txt'
162             self.paths['sha256_sig_url'] = '{0}torbrowser/'+tbb_version+'/sha256sums.txt-mikeperry.asc'
163         else:
164             self.paths = {
165                 'tbl_bin': '/usr/bin/torbrowser-launcher',
166                 'icon_file': os.path.join(SHARE, 'pixmaps/torbrowser80.xpm'),
167                 'torproject_pem': os.path.join(SHARE, 'torbrowser-launcher/torproject.pem'),
168                 'mike_key': os.path.join(SHARE, 'torbrowser-launcher/mike-2013-09.asc'),
169                 'mirrors_txt': [os.path.join(SHARE, 'torbrowser-launcher/mirrors.txt'),
170                                 '/usr/local/share/torbrowser-launcher/mirrors.txt'],
171                 'modem_sound': os.path.join(SHARE, 'torbrowser-launcher/modem.ogg'),
172                 'data_dir': tbb_data,
173                 'download_dir': tbb_data+'/download',
174                 'gnupg_homedir': tbb_data+'/gnupg_homedir',
175                 'settings_file': tbb_data+'/settings',
176                 'update_check_url': 'https://check.torproject.org/RecommendedTBBVersions',
177                 'update_check_file': tbb_data+'/download/RecommendedTBBVersions',
178                 'tbb': {
179                     'dir': tbb_data+'/tbb/'+self.architecture,
180                     'start': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
181                     'versions': tbb_data+'/tbb/'+self.architecture+'/tor-browser_'+self.language+'/Docs/sources/versions',
182                 }
183             }
184
185     # create a directory
186     @staticmethod
187     def mkdir(path):
188         try:
189             if not os.path.exists(path):
190                 os.makedirs(path, 0700)
191                 return True
192         except:
193             print _("Cannot create directory {0}").format(path)
194             return False
195         if not os.access(path, os.W_OK):
196             print _("{0} is not writable").format(path)
197             return False
198         return True
199
200     # if gnupg_homedir isn't set up, set it up
201     def init_gnupg(self):
202         if not os.path.exists(self.paths['gnupg_homedir']):
203             print _('Creating GnuPG homedir'), self.paths['gnupg_homedir']
204             self.mkdir(self.paths['gnupg_homedir'])
205         self.import_keys()
206
207     # import gpg keys
208     def import_keys(self):
209         print _('Importing keys')
210         subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['gnupg_homedir'], '--import', self.paths['mike_key']]).wait()
211
212     # load mirrors
213     def load_mirrors(self):
214         self.mirrors = []
215         for srcfile in self.paths['mirrors_txt']:
216             if os.path.exists(srcfile):
217                 print "Successfully loaded mirrors from %s" % srcfile
218             elif not os.path.exists(srcfile):
219                 print "Warning: can't load mirrors from %s" % srcfile
220                 continue
221             for mirror in open(srcfile, 'r').readlines():
222                 if mirror.strip() not in self.mirrors:
223                     self.mirrors.append(mirror.strip())
224
225     # load settings
226     def load_settings(self):
227         default_settings = {
228             'tbl_version': self.tbl_version,
229             'installed_version': False,
230             'latest_version': '0',
231             'update_over_tor': True,
232             'check_for_updates': False,
233             'modem_sound': False,
234             'last_update_check_timestamp': 0,
235             'mirror': self.default_mirror
236         }
237
238         if os.path.isfile(self.paths['settings_file']):
239             settings = pickle.load(open(self.paths['settings_file']))
240             resave = False
241
242             # settings migrations
243             if settings['tbl_version'] <= '0.1.0':
244                 print '0.1.0 migration'
245                 settings['installed_version'] = settings['installed_version']['stable']
246                 settings['latest_version'] = settings['latest_version']['stable']
247                 resave = True
248
249                 # make new tbb folder
250                 self.mkdir(self.paths['tbb']['dir'])
251                 old_tbb_dir = self.paths['data_dir']+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language
252                 new_tbb_dir = self.paths['tbb']['dir']+'/tor-browser_'+self.language
253                 if os.path.isdir(old_tbb_dir):
254                     os.rename(old_tbb_dir, new_tbb_dir)
255
256             # make sure settings file is up-to-date
257             for setting in default_settings:
258                 if setting not in settings:
259                     settings[setting] = default_settings[setting]
260                     resave = True
261
262             # make sure the version is current
263             if settings['tbl_version'] != self.tbl_version:
264                 settings['tbl_version'] = self.tbl_version
265                 resave = True
266
267             self.settings = settings
268             if resave:
269                 self.save_settings()
270
271         else:
272             self.settings = default_settings
273             self.save_settings()
274
275     # save settings
276     def save_settings(self):
277         pickle.dump(self.settings, open(self.paths['settings_file'], 'w'))
278         return True
279
280     # get the process id of a program
281     @staticmethod
282     def get_pid(bin_path, python=False):
283         pid = None
284
285         for p in psutil.process_iter():
286             try:
287                 if p.pid != os.getpid():
288                     exe = None
289                     if python:
290                         if len(p.cmdline) > 1:
291                             if 'python' in p.cmdline[0]:
292                                 exe = p.cmdline[1]
293                     else:
294                         if len(p.cmdline) > 0:
295                             exe = p.cmdline[0]
296
297                     if exe == bin_path:
298                         pid = p.pid
299
300             except:
301                 pass
302
303         return pid
304
305     # bring program's x window to front
306     @staticmethod
307     def bring_window_to_front(pid):
308         # figure out the window id
309         win_id = None
310         p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
311         for line in p.stdout.readlines():
312             line_split = line.split()
313             cur_win_id = line_split[0]
314             cur_win_pid = int(line_split[2])
315             if cur_win_pid == pid:
316                 win_id = cur_win_id
317
318         # bring to front
319         if win_id:
320             subprocess.call(['wmctrl', '-i', '-a', win_id])
321
322
323 class TBLSettings:
324     def __init__(self, common):
325         print _('Starting settings dialog')
326         self.common = common
327
328         # set up the window
329         self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
330         self.window.set_title(_("Tor Browser Launcher Settings"))
331         self.window.set_icon_from_file(self.common.paths['icon_file'])
332         self.window.set_position(gtk.WIN_POS_CENTER)
333         self.window.set_border_width(10)
334         self.window.connect("delete_event", self.delete_event)
335         self.window.connect("destroy", self.destroy)
336
337         # build the rest of the UI
338         self.box = gtk.VBox(False, 10)
339         self.window.add(self.box)
340         self.box.show()
341
342         self.hbox = gtk.HBox(False, 10)
343         self.box.pack_start(self.hbox, True, True, 0)
344         self.hbox.show()
345
346         self.settings_box = gtk.VBox(False, 10)
347         self.hbox.pack_start(self.settings_box, True, True, 0)
348         self.settings_box.show()
349
350         self.labels_box = gtk.VBox(False, 10)
351         self.hbox.pack_start(self.labels_box, True, True, 0)
352         self.labels_box.show()
353
354         # download over tor
355         try:
356             import txsocksx
357             self.txsocks_found = True
358         except ImportError:
359             self.txsocks_found = False
360         self.tor_update_checkbox = gtk.CheckButton(_("Download updates over Tor (recommended)"))
361         if self.txsocks_found:
362             self.tor_update_checkbox.set_tooltip_text(_("This option is only available when using a system wide Tor installation."))
363         else:
364             self.tor_update_checkbox.set_tooltip_text(_("This option requires the python-txsocksx package."))
365
366         self.settings_box.pack_start(self.tor_update_checkbox, True, True, 0)
367         if self.common.settings['update_over_tor'] and self.txsocks_found:
368             self.tor_update_checkbox.set_active(True)
369         else:
370             self.tor_update_checkbox.set_active(False)
371
372         if self.txsocks_found == False:
373             self.tor_update_checkbox.set_sensitive(False)
374
375         self.tor_update_checkbox.show()
376
377         # check for updates
378         self.update_checkbox = gtk.CheckButton(_("Check for updates next launch"))
379         self.settings_box.pack_start(self.update_checkbox, True, True, 0)
380         if self.common.settings['check_for_updates']:
381             self.update_checkbox.set_active(True)
382         else:
383             self.update_checkbox.set_active(False)
384         self.update_checkbox.show()
385
386         # modem sound
387         self.modem_checkbox = gtk.CheckButton(_("Play modem sound, because Tor is slow :]"))
388         self.settings_box.pack_start(self.modem_checkbox, True, True, 0)
389
390         try:
391             import pygame
392             if self.common.settings['modem_sound']:
393                 self.modem_checkbox.set_active(True)
394             else:
395                 self.modem_checkbox.set_active(False)
396         except ImportError:
397             self.modem_checkbox.set_active(False)
398             self.modem_checkbox.set_sensitive(False)
399             self.modem_checkbox.set_tooltip_text(_("This option requires python-pygame to be installed"))
400         self.modem_checkbox.show()
401
402         # labels
403         if(self.common.settings['installed_version']):
404             self.label1 = gtk.Label(_('Installed version:\n{0}').format(self.common.settings['installed_version']))
405         else:
406             self.label1 = gtk.Label(_('Not installed'))
407         self.label1.set_line_wrap(True)
408         self.labels_box.pack_start(self.label1, True, True, 0)
409         self.label1.show()
410
411         if(self.common.settings['last_update_check_timestamp'] > 0):
412             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']))))
413         else:
414             self.label1 = gtk.Label(_('Never checked for updates'))
415         self.label1.set_line_wrap(True)
416         self.labels_box.pack_start(self.label1, True, True, 0)
417         self.label1.show()
418
419         # mirrors
420         self.mirrors_box = gtk.HBox(False, 10)
421         self.box.pack_start(self.mirrors_box, True, True, 0)
422         self.mirrors_box.show()
423
424         self.mirrors_label = gtk.Label(_('Mirror'))
425         self.mirrors_label.set_line_wrap(True)
426         self.mirrors_box.pack_start(self.mirrors_label, True, True, 0)
427         self.mirrors_label.show()
428
429         self.mirrors = gtk.combo_box_new_text()
430         for mirror in self.common.mirrors:
431             self.mirrors.append_text(mirror)
432         if self.common.settings['mirror'] in self.common.mirrors:
433             self.mirrors.set_active(self.common.mirrors.index(self.common.settings['mirror']))
434         else:
435             self.mirrors.set_active(0)
436         self.mirrors_box.pack_start(self.mirrors, True, True, 0)
437         self.mirrors.show()
438
439         # button box
440         self.button_box = gtk.HButtonBox()
441         self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
442         self.box.pack_start(self.button_box, True, True, 0)
443         self.button_box.show()
444
445         # save and launch button
446         save_launch_image = gtk.Image()
447         save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
448         self.save_launch_button = gtk.Button(_("Launch Tor Browser"))
449         self.save_launch_button.set_image(save_launch_image)
450         self.save_launch_button.connect("clicked", self.save_launch, None)
451         self.button_box.add(self.save_launch_button)
452         self.save_launch_button.show()
453
454         # save and exit button
455         save_exit_image = gtk.Image()
456         save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
457         self.save_exit_button = gtk.Button(_("Save & Exit"))
458         self.save_exit_button.set_image(save_exit_image)
459         self.save_exit_button.connect("clicked", self.save_exit, None)
460         self.button_box.add(self.save_exit_button)
461         self.save_exit_button.show()
462
463         # cancel button
464         cancel_image = gtk.Image()
465         cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
466         self.cancel_button = gtk.Button(_("Cancel"))
467         self.cancel_button.set_image(cancel_image)
468         self.cancel_button.connect("clicked", self.destroy, None)
469         self.button_box.add(self.cancel_button)
470         self.cancel_button.show()
471
472         # show the window
473         self.window.show()
474
475         # start gtk
476         gtk.main()
477
478     # UI Callback for update over tor/use system tor
479     def on_system_tor_clicked(self, event):
480         if self.txsocks_found:
481             value = self.system_tor_checkbox.get_active()
482         else:
483             value = False
484
485         self.tor_update_checkbox.set_active(value)
486         self.tor_update_checkbox.set_sensitive(value)
487
488     # save and launch
489     def save_launch(self, widget, data=None):
490         self.save()
491         subprocess.Popen([self.common.paths['tbl_bin']])
492         self.destroy(False)
493
494     # save and exit
495     def save_exit(self, widget, data=None):
496         self.save()
497         self.destroy(False)
498
499     # save settings
500     def save(self):
501         # checkbox options
502         self.common.settings['update_over_tor'] = self.tor_update_checkbox.get_active()
503         self.common.settings['check_for_updates'] = self.update_checkbox.get_active()
504         self.common.settings['modem_sound'] = self.modem_checkbox.get_active()
505
506         # figure out the selected mirror
507         self.common.settings['mirror'] = self.common.mirrors[self.mirrors.get_active()]
508
509         # save them
510         self.common.save_settings()
511
512     # exit
513     def delete_event(self, widget, event, data=None):
514         return False
515
516     def destroy(self, widget, data=None):
517         gtk.main_quit()
518
519
520 class TBLLauncher:
521     def __init__(self, common):
522         print _('Starting launcher dialog')
523         self.common = common
524
525         # init launcher
526         self.set_gui(None, '', [])
527         self.launch_gui = True
528         print "LATEST VERSION", self.common.settings['latest_version']
529         self.common.build_paths(self.common.settings['latest_version'])
530
531         if self.common.settings['update_over_tor']:
532             try:
533                 import txsocksx
534             except ImportError:
535                 md = gtk.MessageDialog(None, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, _("The python-txsocksx package is missing, downloads will not happen over tor"))
536                 md.set_position(gtk.WIN_POS_CENTER)
537                 md.run()
538                 md.destroy()
539                 self.common.settings['update_over_tor'] = False
540                 self.common.save_settings()
541
542         # is firefox already running?
543         if self.common.settings['installed_version']:
544             firefox_pid = self.common.get_pid('./Browser/firefox')
545             if firefox_pid:
546                 print _('Firefox are is open, bringing to focus')
547                 # bring firefox to front
548                 self.common.bring_window_to_front(firefox_pid)
549                 return
550
551         # check for updates?
552         check_for_updates = False
553         if self.common.settings['check_for_updates']:
554             check_for_updates = True
555
556         if not check_for_updates:
557             # how long was it since the last update check?
558             # 86400 seconds = 24 hours
559             current_timestamp = int(time.time())
560             if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
561                 check_for_updates = True
562
563         if check_for_updates:
564             # check for update
565             print 'Checking for update'
566             self.set_gui('task', _("Checking for Tor Browser update."),
567                          ['download_update_check',
568                           'attempt_update'])
569         else:
570             # no need to check for update
571             print _('Checked for update within 24 hours, skipping')
572             self.start_launcher()
573
574         if self.launch_gui:
575             # set up the window
576             self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
577             self.window.set_title(_("Tor Browser"))
578             self.window.set_icon_from_file(self.common.paths['icon_file'])
579             self.window.set_position(gtk.WIN_POS_CENTER)
580             self.window.set_border_width(10)
581             self.window.connect("delete_event", self.delete_event)
582             self.window.connect("destroy", self.destroy)
583
584             # build the rest of the UI
585             self.build_ui()
586
587     # download or run TBB
588     def start_launcher(self):
589         # is TBB already installed?
590         latest_version = self.common.settings['latest_version']
591         installed_version = self.common.settings['installed_version']
592
593         # verify installed version for newer versions of TBB (#58)
594         if installed_version >= '3.0':
595             versions_filename = self.common.paths['tbb']['versions']
596             if os.path.exists(versions_filename):
597                 for line in open(versions_filename):
598                     if 'TORBROWSER_VERSION' in line:
599                         installed_version = line.lstrip('TORBROWSER_VERSION=').strip()
600
601         start = self.common.paths['tbb']['start']
602         if os.path.isfile(start) and os.access(start, os.X_OK):
603             if installed_version == latest_version:
604                 print _('Latest version of TBB is installed, launching')
605                 # current version of tbb is installed, launch it
606                 self.run(False)
607                 self.launch_gui = False
608             elif installed_version < latest_version:
609                 print _('TBB is out of date, attempting to upgrade to {0}'.format(latest_version))
610                 # there is a tbb upgrade available
611                 self.set_gui('task', _("Your Tor Browser is out of date."),
612                              ['download_sha256',
613                               'download_sha256_sig',
614                               'download_tarball',
615                               'verify',
616                               'extract',
617                               'run'])
618             else:
619                 # for some reason the installed tbb is newer than the current version?
620                 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
621
622         # not installed
623         else:
624             print _('TBB is not installed, attempting to install {0}'.format(latest_version))
625             self.set_gui('task', _("Downloading and installing Tor Browser."),
626                          ['download_sha256',
627                           'download_sha256_sig',
628                           'download_tarball',
629                           'verify',
630                           'extract',
631                           'run'])
632
633     # there are different GUIs that might appear, this sets which one we want
634     def set_gui(self, gui, message, tasks, autostart=True):
635         self.gui = gui
636         self.gui_message = message
637         self.gui_tasks = tasks
638         self.gui_task_i = 0
639         self.gui_autostart = autostart
640
641     # set all gtk variables to False
642     def clear_ui(self):
643         if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
644             self.box.destroy()
645         self.box = False
646
647         self.label = False
648         self.progressbar = False
649         self.button_box = False
650         self.start_button = False
651         self.exit_button = False
652
653     # build the application's UI
654     def build_ui(self):
655         self.clear_ui()
656
657         self.box = gtk.VBox(False, 20)
658         self.window.add(self.box)
659
660         if 'error' in self.gui:
661             # labels
662             self.label = gtk.Label(self.gui_message)
663             self.label.set_line_wrap(True)
664             self.box.pack_start(self.label, True, True, 0)
665             self.label.show()
666
667             # button box
668             self.button_box = gtk.HButtonBox()
669             self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
670             self.box.pack_start(self.button_box, True, True, 0)
671             self.button_box.show()
672
673             if self.gui != 'error':
674                 # yes button
675                 yes_image = gtk.Image()
676                 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
677                 self.yes_button = gtk.Button("Yes")
678                 self.yes_button.set_image(yes_image)
679                 if self.gui == 'error_try_stable':
680                     self.yes_button.connect("clicked", self.try_stable, None)
681                 elif self.gui == 'error_try_default_mirror':
682                     self.yes_button.connect("clicked", self.try_default_mirror, None)
683                 elif self.gui == 'error_try_tor':
684                     self.yes_button.connect("clicked", self.try_tor, None)
685                 self.button_box.add(self.yes_button)
686                 self.yes_button.show()
687
688             # exit button
689             exit_image = gtk.Image()
690             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
691             self.exit_button = gtk.Button("Exit")
692             self.exit_button.set_image(exit_image)
693             self.exit_button.connect("clicked", self.destroy, None)
694             self.button_box.add(self.exit_button)
695             self.exit_button.show()
696
697         elif self.gui == 'task':
698             # label
699             self.label = gtk.Label(self.gui_message)
700             self.label.set_line_wrap(True)
701             self.box.pack_start(self.label, True, True, 0)
702             self.label.show()
703
704             # progress bar
705             self.progressbar = gtk.ProgressBar(adjustment=None)
706             self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
707             self.progressbar.set_pulse_step(0.01)
708             self.box.pack_start(self.progressbar, True, True, 0)
709
710             # button box
711             self.button_box = gtk.HButtonBox()
712             self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
713             self.box.pack_start(self.button_box, True, True, 0)
714             self.button_box.show()
715
716             # start button
717             start_image = gtk.Image()
718             start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
719             self.start_button = gtk.Button(_("Start"))
720             self.start_button.set_image(start_image)
721             self.start_button.connect("clicked", self.start, None)
722             self.button_box.add(self.start_button)
723             if not self.gui_autostart:
724                 self.start_button.show()
725
726             # exit button
727             exit_image = gtk.Image()
728             exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
729             self.exit_button = gtk.Button(_("Exit"))
730             self.exit_button.set_image(exit_image)
731             self.exit_button.connect("clicked", self.destroy, None)
732             self.button_box.add(self.exit_button)
733             self.exit_button.show()
734
735         self.box.show()
736         self.window.show()
737
738         if self.gui_autostart:
739             self.start(None)
740
741     # start button clicked, begin tasks
742     def start(self, widget, data=None):
743         # disable the start button
744         if self.start_button:
745             self.start_button.set_sensitive(False)
746
747         # start running tasks
748         self.run_task()
749
750     # run the next task in the task list
751     def run_task(self):
752         self.refresh_gtk()
753
754         if self.gui_task_i >= len(self.gui_tasks):
755             self.destroy(False)
756             return
757
758         task = self.gui_tasks[self.gui_task_i]
759
760         # get ready for the next task
761         self.gui_task_i += 1
762
763         print _('Running task: {0}'.format(task))
764         if task == 'download_update_check':
765             print _('Downloading'), self.common.paths['update_check_url']
766             self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
767
768         if task == 'attempt_update':
769             print _('Checking to see if update is needed')
770             self.attempt_update()
771
772         elif task == 'download_sha256':
773             print _('Downloading'), self.common.paths['sha256_url'].format(self.common.settings['mirror'])
774             self.download('signature', self.common.paths['sha256_url'], self.common.paths['sha256_file'])
775
776         elif task == 'download_sha256_sig':
777             print _('Downloading'), self.common.paths['sha256_sig_url'].format(self.common.settings['mirror'])
778             self.download('signature', self.common.paths['sha256_sig_url'], self.common.paths['sha256_sig_file'])
779
780         elif task == 'download_tarball':
781             print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
782             self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
783
784         elif task == 'verify':
785             print _('Verifying signature')
786             self.verify()
787
788         elif task == 'extract':
789             print _('Extracting'), self.common.paths['tarball_filename']
790             self.extract()
791
792         elif task == 'run':
793             print _('Running'), self.common.paths['tbb']['start']
794             self.run()
795
796         elif task == 'start_over':
797             print _('Starting download over again')
798             self.start_over()
799
800     def response_received(self, response):
801         class FileDownloader(Protocol):
802             def __init__(self, common, file, total, progress, done_cb):
803                 self.file = file
804                 self.total = total
805                 self.so_far = 0
806                 self.progress = progress
807                 self.all_done = done_cb
808
809                 if response.code != 200:
810                     if common.settings['mirror'] != common.default_mirror:
811                         raise TryDefaultMirrorException(_("Download Error: {0} {1}\n\nYou are currently using a non-default mirror:\n{2}\n\nWould you like to switch back to the default?").format(response.code, response.phrase, common.settings['mirror']))
812                     else:
813                         raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
814
815             def dataReceived(self, bytes):
816                 self.file.write(bytes)
817                 self.so_far += len(bytes)
818                 percent = float(self.so_far) / float(self.total)
819                 self.progress.set_fraction(percent)
820                 amount = float(self.so_far)
821                 units = "bytes"
822                 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
823                     if amount > size:
824                         units = unit
825                         amount = amount / float(size)
826                         break
827
828                 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
829
830             def connectionLost(self, reason):
831                 print _('Finished receiving body:'), reason.getErrorMessage()
832                 self.all_done(reason)
833
834         dl = FileDownloader(self.common, self.file_download, response.length, self.progressbar, self.response_finished)
835         response.deliverBody(dl)
836
837     def response_finished(self, msg):
838         if msg.check(ResponseDone):
839             self.file_download.close()
840             delattr(self, 'current_download_path')
841
842             # next task!
843             self.run_task()
844
845         else:
846             print "FINISHED", msg
847             ## FIXME handle errors
848
849     def download_error(self, f):
850         print _("Download error:"), f.value, type(f.value)
851
852         if isinstance(f.value, TryStableException):
853             f.trap(TryStableException)
854             self.set_gui('error_try_stable', str(f.value), [], False)
855
856         elif isinstance(f.value, TryDefaultMirrorException):
857             f.trap(TryDefaultMirrorException)
858             self.set_gui('error_try_default_mirror', str(f.value), [], False)
859
860         elif isinstance(f.value, DownloadErrorException):
861             f.trap(DownloadErrorException)
862             self.set_gui('error', str(f.value), [], False)
863
864         elif isinstance(f.value, DNSLookupError):
865             f.trap(DNSLookupError)
866             if common.settings['mirror'] != common.default_mirror:
867                 self.set_gui('error_try_default_mirror', _("DNS Lookup Error\n\nYou are currently using a non-default mirror:\n{0}\n\nWould you like to switch back to the default?").format(common.settings['mirror']), [], False)
868             else:
869                 self.set_gui('error', str(f.value), [], False)
870
871         elif isinstance(f.value, ResponseFailed):
872             for reason in f.value.reasons:
873                 if isinstance(reason.value, OpenSSL.SSL.Error):
874                     # TODO: add the ability to report attack by posting bug to trac.torproject.org
875                     if not self.common.settings['update_over_tor']:
876                         self.set_gui('error_try_tor', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack. Try the download again using Tor?'), [], False)
877                     else:
878                         self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
879
880         else:
881             self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
882
883         self.build_ui()
884
885     def download(self, name, url, path):
886         # keep track of current download
887         self.current_download_path = path
888
889         # initialize the progress bar
890         mirror_url = url.format(self.common.settings['mirror'])
891         self.progressbar.set_fraction(0)
892         self.progressbar.set_text(_('Downloading {0}').format(name))
893         self.progressbar.show()
894         self.refresh_gtk()
895
896         if self.common.settings['update_over_tor']:
897             print _('Updating over Tor')
898             from twisted.internet.endpoints import TCP4ClientEndpoint
899             from txsocksx.http import SOCKS5Agent
900
901             torEndpoint = TCP4ClientEndpoint(reactor, '127.0.0.1', 9050)
902
903             # default mirror gets certificate pinning, only for requests that use the mirror
904             if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
905                 agent = SOCKS5Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']), proxyEndpoint=torEndpoint)
906             else:
907                 agent = SOCKS5Agent(reactor, proxyEndpoint=torEndpoint)
908         else:
909             if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
910                 agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
911             else:
912                 agent = Agent(reactor)
913
914         # actually, agent needs to follow redirect
915         agent = RedirectAgent(agent)
916
917         # start the request
918         d = agent.request('GET', mirror_url,
919                           Headers({'User-Agent': ['torbrowser-launcher']}),
920                           None)
921
922         self.file_download = open(path, 'w')
923         d.addCallback(self.response_received).addErrback(self.download_error)
924
925         if not reactor.running:
926             reactor.run()
927
928     def try_default_mirror(self, widget, data=None):
929         # change mirror to default and relaunch TBL
930         self.common.settings['mirror'] = self.common.default_mirror
931         self.common.save_settings()
932         subprocess.Popen([self.common.paths['tbl_bin']])
933         self.destroy(False)
934
935     def try_tor(self, widget, data=None):
936         # set update_over_tor to true and relaunch TBL
937         self.common.settings['update_over_tor'] = True
938         self.common.save_settings()
939         subprocess.Popen([self.common.paths['tbl_bin']])
940         self.destroy(False)
941
942     def attempt_update(self):
943         # load the update check file
944         try:
945             versions = json.load(open(self.common.paths['update_check_file']))
946             latest = None
947
948             # filter linux versions
949             valid = []
950             for version in versions:
951                 if '-Linux' in version:
952                     valid.append(str(version))
953             valid.sort()
954             if len(valid):
955                 latest = valid.pop()
956
957             if latest:
958                 self.common.settings['latest_version'] = latest[:-len('-Linux')]
959                 self.common.settings['last_update_check_timestamp'] = int(time.time())
960                 self.common.settings['check_for_updates'] = False
961                 self.common.save_settings()
962                 self.common.build_paths(self.common.settings['latest_version'])
963                 self.start_launcher()
964
965             else:
966                 # failed to find the latest version
967                 self.set_gui('error', _("Error checking for updates."), [], False)
968
969         except:
970             # not a valid JSON object
971             self.set_gui('error', _("Error checking for updates."), [], False)
972
973         # now start over
974         self.clear_ui()
975         self.build_ui()
976
977     def verify(self):
978         # initialize the progress bar
979         self.progressbar.set_fraction(0)
980         self.progressbar.set_text(_('Verifying Signature'))
981         self.progressbar.show()
982
983         verified = False
984         # check the sha256 file's sig, and also take the sha256 of the tarball and compare
985         p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']])
986         self.pulse_until_process_exits(p)
987         if p.returncode == 0:
988             # compare with sha256 of the tarball
989             tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
990             for line in open(self.common.paths['sha256_file'], 'r').readlines():
991                 if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
992                     verified = True
993
994         if verified:
995             self.run_task()
996         else:
997             # TODO: add the ability to report attack by posting bug to trac.torproject.org
998             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)
999             self.clear_ui()
1000             self.build_ui()
1001
1002             if not reactor.running:
1003                 reactor.run()
1004
1005     def extract(self):
1006         # initialize the progress bar
1007         self.progressbar.set_fraction(0)
1008         self.progressbar.set_text(_('Installing'))
1009         self.progressbar.show()
1010         self.refresh_gtk()
1011
1012         extracted = False
1013         try:
1014             if self.common.paths['tarball_file'][-2:] == 'xz':
1015                 # if tarball is .tar.xz
1016                 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
1017                 tf = tarfile.open(fileobj=xz)
1018                 tf.extractall(self.common.paths['tbb']['dir'])
1019                 extracted = True
1020             else:
1021                 # if tarball is .tar.gz
1022                 if tarfile.is_tarfile(self.common.paths['tarball_file']):
1023                     tf = tarfile.open(self.common.paths['tarball_file'])
1024                     tf.extractall(self.common.paths['tbb']['dir'])
1025                     extracted = True
1026         except:
1027             pass
1028
1029         if not extracted:
1030             self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
1031             self.clear_ui()
1032             self.build_ui()
1033             return
1034
1035         # installation is finished, so save installed_version
1036         self.common.settings['installed_version'] = self.common.settings['latest_version']
1037         self.common.save_settings()
1038
1039         self.run_task()
1040
1041     def run(self, run_next_task=True):
1042         devnull = open('/dev/null', 'w')
1043         subprocess.Popen([self.common.paths['tbb']['start']], stdout=devnull, stderr=devnull)
1044
1045         # play modem sound?
1046         if self.common.settings['modem_sound']:
1047             try:
1048                 import pygame
1049                 pygame.mixer.init()
1050                 sound = pygame.mixer.Sound(self.common.paths['modem_sound'])
1051                 sound.play()
1052                 time.sleep(10)
1053             except ImportError:
1054                 md = gtk.MessageDialog(None, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, _("The python-pygame package is missing, the modem sound is unavailable."))
1055                 md.set_position(gtk.WIN_POS_CENTER)
1056                 md.run()
1057                 md.destroy()
1058
1059         if run_next_task:
1060             self.run_task()
1061
1062     # make the progress bar pulse until process p (a Popen object) finishes
1063     def pulse_until_process_exits(self, p):
1064         while p.poll() is None:
1065             time.sleep(0.01)
1066             self.progressbar.pulse()
1067             self.refresh_gtk()
1068
1069     # start over and download TBB again
1070     def start_over(self):
1071         self.label.set_text(_("Downloading Tor Browser Bundle over again."))
1072         self.gui_tasks = ['download_tarball', 'verify', 'extract', 'run']
1073         self.gui_task_i = 0
1074         self.start(None)
1075
1076     # refresh gtk
1077     def refresh_gtk(self):
1078         while gtk.events_pending():
1079             gtk.main_iteration(False)
1080
1081     # exit
1082     def delete_event(self, widget, event, data=None):
1083         return False
1084
1085     def destroy(self, widget, data=None):
1086         if hasattr(self, 'file_download'):
1087             self.file_download.close()
1088         if hasattr(self, 'current_download_path'):
1089             os.remove(self.current_download_path)
1090             delattr(self, 'current_download_path')
1091         if reactor.running:
1092             reactor.stop()
1093
1094 if __name__ == "__main__":
1095     with open(os.path.join(SHARE, 'torbrowser-launcher/version')) as buf:
1096         tor_browser_launcher_version = buf.read().strip()
1097
1098     print _('Tor Browser Launcher')
1099     print _('By Micah Lee, licensed under GPLv3')
1100     print _('version {0}').format(tor_browser_launcher_version)
1101     print 'https://github.com/micahflee/torbrowser-launcher'
1102
1103     common = TBLCommon(tor_browser_launcher_version)
1104
1105     # is torbrowser-launcher already running?
1106     tbl_pid = common.get_pid(common.paths['tbl_bin'], True)
1107     if tbl_pid:
1108         print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
1109         common.bring_window_to_front(tbl_pid)
1110         sys.exit()
1111
1112     if '-settings' in sys.argv:
1113         # settings mode
1114         app = TBLSettings(common)
1115
1116     else:
1117         # launcher mode
1118         app = TBLLauncher(common)