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