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