4 https://github.com/micahflee/torbrowser-launcher/
6 Copyright (c) 2013-2014 Micah Lee <micah@micahflee.com>
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
17 The above copyright notice and this permission notice shall be
18 included in all copies or substantial portions of the Software.
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.
34 gettext.install('torbrowser-launcher', '/usr/share/torbrowser-launcher/locale')
36 from twisted.internet import gtk2reactor
38 from twisted.internet import reactor
44 import os, subprocess, locale, time, pickle, json, tarfile, psutil, hashlib, lzma
46 from twisted.web.client import Agent, RedirectAgent, ResponseDone, ResponseFailed
47 from twisted.web.http_headers import Headers
48 from twisted.internet.protocol import Protocol
49 from twisted.internet.ssl import ClientContextFactory
50 from twisted.internet.error import DNSLookupError
55 class TryStableException(Exception):
59 class TryDefaultMirrorException(Exception):
63 class DownloadErrorException(Exception):
67 class VerifyTorProjectCert(ClientContextFactory):
69 def __init__(self, torproject_pem):
70 self.torproject_ca = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, open(torproject_pem, 'r').read())
72 def getContext(self, host, port):
73 ctx = ClientContextFactory.getContext(self)
74 ctx.set_verify_depth(0)
75 ctx.set_verify(OpenSSL.SSL.VERIFY_PEER | OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname)
78 def verifyHostname(self, connection, cert, errno, depth, preverifyOK):
79 return cert.digest('sha256') == self.torproject_ca.digest('sha256')
84 def __init__(self, tbl_version):
85 print _('Initializing Tor Browser Launcher')
86 self.tbl_version = tbl_version
89 self.available_versions = {
90 'stable': _('Tor Browser Bundle - stable'),
91 'alpha': _('Tor Browser Bundle - alpha')
93 self.default_mirror = 'https://www.torproject.org/dist/'
95 self.discover_arch_lang()
97 self.mkdir(self.paths['data_dir'])
100 self.mkdir(self.paths['download_dir'])
101 self.mkdir(self.paths['tbb'][self.settings['preferred']]['dir'])
104 # allow buttons to have icons
106 gtk_settings = gtk.settings_get_default()
107 gtk_settings.props.gtk_button_images = True
111 # discover the architecture and language
112 def discover_arch_lang(self):
113 # figure out the architecture
114 self.architecture = 'x86_64' if '64' in platform.architecture()[0] else 'i686'
116 # figure out the language
117 available_languages = ['en-US', 'ar', 'de', 'es-ES', 'fa', 'fr', 'it', 'ko', 'nl', 'pl', 'pt-PT', 'ru', 'vi', 'zh-CN']
118 default_locale = locale.getdefaultlocale()[0]
119 if default_locale is None:
120 self.language = 'en-US'
122 self.language = default_locale.replace('_', '-')
123 if self.language not in available_languages:
124 self.language = self.language.split('-')[0]
125 if self.language not in available_languages:
126 for l in available_languages:
127 if l[0:2] == self.language:
129 # if language isn't available, default to english
130 if self.language not in available_languages:
131 self.language = 'en-US'
133 # build all relevant paths
134 def build_paths(self, tbb_version=None):
135 homedir = os.getenv('HOME')
137 homedir = '/tmp/.torbrowser-'+os.getenv('USER')
138 if not os.path.exists(homedir):
140 os.mkdir(homedir, 0700)
142 self.set_gui('error', _("Error creating {0}").format(homedir), [], False)
143 if not os.access(homedir, os.W_OK):
144 self.set_gui('error', _("{0} is not writable").format(homedir), [], False)
146 tbb_data = '%s/.torbrowser' % homedir
150 dirname = tbb_version.replace('-alpha-', 'a').replace('-beta-', 'b').replace('-rc-', 'rc')
151 if self.architecture == 'x86_64':
155 tarball_filename = 'tor-browser-'+arch+'-'+tbb_version+'_'+self.language+'.tar.xz'
158 self.paths['tarball_url'] = '{0}torbrowser/'+dirname+'/'+tarball_filename
159 self.paths['tarball_file'] = tbb_data+'/download/'+tarball_filename
160 self.paths['tarball_filename'] = tarball_filename
163 self.paths['sha256_file'] = tbb_data+'/download/sha256sums.txt'
164 self.paths['sha256_sig_file'] = tbb_data+'/download/sha256sums.txt.asc'
165 self.paths['sha256_url'] = '{0}torbrowser/'+dirname+'/sha256sums.txt'
166 self.paths['sha256_sig_url'] = '{0}torbrowser/'+dirname+'/sha256sums.txt-mikeperry.asc'
169 'tbl_bin': '/usr/bin/torbrowser-launcher',
170 'icon_file': '/usr/share/pixmaps/torbrowser80.xpm',
171 'torproject_pem': '/usr/share/torbrowser-launcher/torproject.pem',
172 'erinn_key': '/usr/share/torbrowser-launcher/erinn.asc',
173 'sebastian_key': '/usr/share/torbrowser-launcher/sebastian.asc',
174 'alexandre_key': '/usr/share/torbrowser-launcher/alexandre.asc',
175 'mike_key': '/usr/share/torbrowser-launcher/mike-2013-09.asc',
176 'mirrors_txt': ['/usr/share/torbrowser-launcher/mirrors.txt',
177 '/usr/local/share/torbrowser-launcher/mirrors.txt'],
178 'modem_sound': '/usr/share/torbrowser-launcher/modem.ogg',
179 'data_dir': tbb_data,
180 'download_dir': tbb_data+'/download',
181 'gnupg_homedir': tbb_data+'/gnupg_homedir',
182 'settings_file': tbb_data+'/settings',
183 'update_check_url': 'https://check.torproject.org/RecommendedTBBVersions',
184 'update_check_file': tbb_data+'/download/RecommendedTBBVersions',
187 'dir': tbb_data+'/tbb/stable/'+self.architecture,
188 'start': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
189 'versions': tbb_data+'/tbb/stable/'+self.architecture+'/tor-browser_'+self.language+'/Docs/sources/versions',
192 'dir': tbb_data+'/tbb/alpha/'+self.architecture,
193 'start': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/start-tor-browser',
194 'versions': tbb_data+'/tbb/alpha/'+self.architecture+'/tor-browser_'+self.language+'/Docs/sources/versions',
203 if not os.path.exists(path):
204 os.makedirs(path, 0700)
207 print _("Cannot create directory {0}").format(path)
209 if not os.access(path, os.W_OK):
210 print _("{0} is not writable").format(path)
214 # if gnupg_homedir isn't set up, set it up
215 def init_gnupg(self):
216 if not os.path.exists(self.paths['gnupg_homedir']):
217 print _('Creating GnuPG homedir'), self.paths['gnupg_homedir']
218 self.mkdir(self.paths['gnupg_homedir'])
222 def import_keys(self):
223 print _('Importing keys')
224 subprocess.Popen(['/usr/bin/gpg', '--homedir', self.paths['gnupg_homedir'], '--import', self.paths['erinn_key'], self.paths['sebastian_key'], self.paths['alexandre_key'], self.paths['mike_key']]).wait()
227 def load_mirrors(self):
229 for srcfile in self.paths['mirrors_txt']:
230 if not os.path.exists(srcfile):
231 print "Warning: can't load mirrors from %s" % srcfile
233 for mirror in open(srcfile, 'r').readlines():
234 if mirror.strip() not in self.mirrors:
235 self.mirrors.append(mirror.strip())
238 def load_settings(self):
240 'tbl_version': self.tbl_version,
241 'preferred': 'stable',
242 'installed_version': {
250 'update_over_tor': True,
251 'check_for_updates': False,
252 'modem_sound': False,
253 'last_update_check_timestamp': 0,
254 'mirror': self.default_mirror
257 if os.path.isfile(self.paths['settings_file']):
258 settings = pickle.load(open(self.paths['settings_file']))
261 # settings migrations
262 if settings['tbl_version'] == '0.0.1':
263 print '0.0.1 migration'
264 self.settings = default_settings
265 self.settings['installed_version']['alpha'] = settings['installed_version']
269 self.mkdir(self.paths['tbb']['alpha']['dir'])
271 # make sure settings file is up-to-date
272 for setting in default_settings:
273 if setting not in settings:
274 settings[setting] = default_settings[setting]
277 # make sure the version is current
278 if settings['tbl_version'] != self.tbl_version:
279 settings['tbl_version'] = self.tbl_version
282 self.settings = settings
287 self.settings = default_settings
291 def save_settings(self):
292 pickle.dump(self.settings, open(self.paths['settings_file'], 'w'))
295 # get the process id of a program
297 def get_pid(bin_path, python=False):
300 for p in psutil.process_iter():
302 if p.pid != os.getpid():
305 if len(p.cmdline) > 1:
306 if 'python' in p.cmdline[0]:
309 if len(p.cmdline) > 0:
320 # bring program's x window to front
322 def bring_window_to_front(pid):
323 # figure out the window id
325 p = subprocess.Popen(['wmctrl', '-l', '-p'], stdout=subprocess.PIPE)
326 for line in p.stdout.readlines():
327 line_split = line.split()
328 cur_win_id = line_split[0]
329 cur_win_pid = int(line_split[2])
330 if cur_win_pid == pid:
335 subprocess.call(['wmctrl', '-i', '-a', win_id])
339 def __init__(self, common):
340 print _('Starting settings dialog')
344 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
345 self.window.set_title(_("Tor Browser Launcher Settings"))
346 self.window.set_icon_from_file(self.common.paths['icon_file'])
347 self.window.set_position(gtk.WIN_POS_CENTER)
348 self.window.set_border_width(10)
349 self.window.connect("delete_event", self.delete_event)
350 self.window.connect("destroy", self.destroy)
352 # build the rest of the UI
353 self.box = gtk.VBox(False, 10)
354 self.window.add(self.box)
357 self.hbox = gtk.HBox(False, 10)
358 self.box.pack_start(self.hbox, True, True, 0)
361 self.settings_box = gtk.VBox(False, 10)
362 self.hbox.pack_start(self.settings_box, True, True, 0)
363 self.settings_box.show()
365 self.labels_box = gtk.VBox(False, 10)
366 self.hbox.pack_start(self.labels_box, True, True, 0)
367 self.labels_box.show()
370 self.preferred_box = gtk.HBox(False, 10)
371 self.settings_box.pack_start(self.preferred_box, True, True, 0)
372 self.preferred_box.show()
374 self.preferred_label = gtk.Label(_('I prefer'))
375 self.preferred_label.set_line_wrap(True)
376 self.preferred_box.pack_start(self.preferred_label, True, True, 0)
377 self.preferred_label.show()
379 self.preferred_options = []
380 for i in self.common.available_versions:
381 self.preferred_options.append(self.common.available_versions[i])
382 self.preferred_options.sort()
384 self.preferred = gtk.combo_box_new_text()
385 for option in self.preferred_options:
386 self.preferred.append_text(option)
387 if self.common.settings['preferred'] in self.common.available_versions:
388 self.preferred.set_active(self.preferred_options.index(self.common.available_versions[self.common.settings['preferred']]))
390 self.preferred.set_active(0)
391 self.preferred_box.pack_start(self.preferred, True, True, 0)
392 self.preferred.show()
397 self.txsocks_found = True
399 self.txsocks_found = False
400 self.tor_update_checkbox = gtk.CheckButton(_("Download updates over Tor (recommended)"))
401 if self.txsocks_found:
402 self.tor_update_checkbox.set_tooltip_text(_("This option is only available when using a system wide Tor installation."))
404 self.tor_update_checkbox.set_tooltip_text(_("This option requires the python-txsocksx package."))
406 self.settings_box.pack_start(self.tor_update_checkbox, True, True, 0)
407 if self.common.settings['update_over_tor'] and self.txsocks_found:
408 self.tor_update_checkbox.set_active(True)
410 self.tor_update_checkbox.set_active(False)
412 if self.txsocks_found == False:
413 self.tor_update_checkbox.set_sensitive(False)
415 self.tor_update_checkbox.show()
418 self.update_checkbox = gtk.CheckButton(_("Check for updates next launch"))
419 self.settings_box.pack_start(self.update_checkbox, True, True, 0)
420 if self.common.settings['check_for_updates']:
421 self.update_checkbox.set_active(True)
423 self.update_checkbox.set_active(False)
424 self.update_checkbox.show()
427 self.modem_checkbox = gtk.CheckButton(_("Play modem sound, because Tor is slow :]"))
428 self.settings_box.pack_start(self.modem_checkbox, True, True, 0)
432 if self.common.settings['modem_sound']:
433 self.modem_checkbox.set_active(True)
435 self.modem_checkbox.set_active(False)
437 self.modem_checkbox.set_active(False)
438 self.modem_checkbox.set_sensitive(False)
439 self.modem_checkbox.set_tooltip_text(_("This option requires python-pygame to be installed"))
440 self.modem_checkbox.show()
443 if(self.common.settings['installed_version'][self.common.settings['preferred']]):
444 self.label1 = gtk.Label(_('Installed version:\n{0}').format(self.common.settings['installed_version'][self.common.settings['preferred']]))
446 self.label1 = gtk.Label(_('Not installed'))
447 self.label1.set_line_wrap(True)
448 self.labels_box.pack_start(self.label1, True, True, 0)
451 if(self.common.settings['last_update_check_timestamp'] > 0):
452 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']))))
454 self.label1 = gtk.Label(_('Never checked for updates'))
455 self.label1.set_line_wrap(True)
456 self.labels_box.pack_start(self.label1, True, True, 0)
460 self.mirrors_box = gtk.HBox(False, 10)
461 self.box.pack_start(self.mirrors_box, True, True, 0)
462 self.mirrors_box.show()
464 self.mirrors_label = gtk.Label(_('Mirror'))
465 self.mirrors_label.set_line_wrap(True)
466 self.mirrors_box.pack_start(self.mirrors_label, True, True, 0)
467 self.mirrors_label.show()
469 self.mirrors = gtk.combo_box_new_text()
470 for mirror in self.common.mirrors:
471 self.mirrors.append_text(mirror)
472 if self.common.settings['mirror'] in self.common.mirrors:
473 self.mirrors.set_active(self.common.mirrors.index(self.common.settings['mirror']))
475 self.preferred.set_active(0)
476 self.mirrors_box.pack_start(self.mirrors, True, True, 0)
480 self.button_box = gtk.HButtonBox()
481 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
482 self.box.pack_start(self.button_box, True, True, 0)
483 self.button_box.show()
485 # save and launch button
486 save_launch_image = gtk.Image()
487 save_launch_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
488 self.save_launch_button = gtk.Button(_("Launch Tor Browser"))
489 self.save_launch_button.set_image(save_launch_image)
490 self.save_launch_button.connect("clicked", self.save_launch, None)
491 self.button_box.add(self.save_launch_button)
492 self.save_launch_button.show()
494 # save and exit button
495 save_exit_image = gtk.Image()
496 save_exit_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
497 self.save_exit_button = gtk.Button(_("Save & Exit"))
498 self.save_exit_button.set_image(save_exit_image)
499 self.save_exit_button.connect("clicked", self.save_exit, None)
500 self.button_box.add(self.save_exit_button)
501 self.save_exit_button.show()
504 cancel_image = gtk.Image()
505 cancel_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
506 self.cancel_button = gtk.Button(_("Cancel"))
507 self.cancel_button.set_image(cancel_image)
508 self.cancel_button.connect("clicked", self.destroy, None)
509 self.button_box.add(self.cancel_button)
510 self.cancel_button.show()
518 # UI Callback for update over tor/use system tor
519 def on_system_tor_clicked(self, event):
520 if self.txsocks_found:
521 value = self.system_tor_checkbox.get_active()
525 self.tor_update_checkbox.set_active(value)
526 self.tor_update_checkbox.set_sensitive(value)
529 def save_launch(self, widget, data=None):
531 subprocess.Popen([self.common.paths['tbl_bin']])
535 def save_exit(self, widget, data=None):
541 # figure out the selected preferred option
543 selected = self.preferred_options[self.preferred.get_active()]
544 for i in self.common.available_versions:
545 if self.common.available_versions[i] == selected:
548 self.common.settings['preferred'] = preferred
551 self.common.settings['update_over_tor'] = self.tor_update_checkbox.get_active()
552 self.common.settings['check_for_updates'] = self.update_checkbox.get_active()
553 self.common.settings['modem_sound'] = self.modem_checkbox.get_active()
555 # figure out the selected mirror
556 self.common.settings['mirror'] = self.common.mirrors[self.mirrors.get_active()]
559 self.common.save_settings()
562 def delete_event(self, widget, event, data=None):
565 def destroy(self, widget, data=None):
570 def __init__(self, common):
571 print _('Starting launcher dialog')
575 self.set_gui(None, '', [])
576 self.launch_gui = True
577 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
579 if self.common.settings['update_over_tor']:
583 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"))
584 md.set_position(gtk.WIN_POS_CENTER)
587 self.common.settings['update_over_tor'] = False
588 self.common.save_settings()
590 # is firefox already running?
591 if self.common.settings['installed_version']:
592 firefox_pid = self.common.get_pid('./Browser/firefox')
594 print _('Firefox are is open, bringing to focus')
595 # bring firefox to front
596 self.common.bring_window_to_front(firefox_pid)
600 check_for_updates = False
601 if self.common.settings['check_for_updates']:
602 check_for_updates = True
604 if not check_for_updates:
605 # how long was it since the last update check?
606 # 86400 seconds = 24 hours
607 current_timestamp = int(time.time())
608 if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
609 check_for_updates = True
611 if check_for_updates:
613 print 'Checking for update'
614 self.set_gui('task', _("Checking for Tor Browser update."),
615 ['download_update_check',
618 # no need to check for update
619 print _('Checked for update within 24 hours, skipping')
620 self.start_launcher()
624 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
625 self.window.set_title(_("Tor Browser"))
626 self.window.set_icon_from_file(self.common.paths['icon_file'])
627 self.window.set_position(gtk.WIN_POS_CENTER)
628 self.window.set_border_width(10)
629 self.window.connect("delete_event", self.delete_event)
630 self.window.connect("destroy", self.destroy)
632 # build the rest of the UI
635 # download or run TBB
636 def start_launcher(self):
637 # is TBB already installed?
638 latest_version = self.common.settings['latest_version'][self.common.settings['preferred']]
639 installed_version = self.common.settings['installed_version'][self.common.settings['preferred']]
641 # verify installed version for newer versions of TBB (#58)
642 if installed_version >= '3.0':
643 versions_filename = self.common.paths['tbb'][self.common.settings['preferred']]['versions']
644 if os.path.exists(versions_filename):
645 for line in open(versions_filename):
646 if 'TORBROWSER_VERSION' in line:
647 installed_version = line.lstrip('TORBROWSER_VERSION=').strip()
649 start = self.common.paths['tbb'][self.common.settings['preferred']]['start']
650 if os.path.isfile(start) and os.access(start, os.X_OK):
651 if installed_version == latest_version:
652 print _('Latest version of TBB is installed, launching')
653 # current version of tbb is installed, launch it
655 self.launch_gui = False
656 elif installed_version < latest_version:
657 print _('TBB is out of date, attempting to upgrade to {0}'.format(latest_version))
658 # there is a tbb upgrade available
659 self.set_gui('task', _("Your Tor Browser is out of date."),
661 'download_sha256_sig',
667 # for some reason the installed tbb is newer than the current version?
668 self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
672 print _('TBB is not installed, attempting to install {0}'.format(latest_version))
673 self.set_gui('task', _("Downloading and installing Tor Browser."),
675 'download_sha256_sig',
681 # there are different GUIs that might appear, this sets which one we want
682 def set_gui(self, gui, message, tasks, autostart=True):
684 self.gui_message = message
685 self.gui_tasks = tasks
687 self.gui_autostart = autostart
689 # set all gtk variables to False
691 if hasattr(self, 'box') and hasattr(self.box, 'destroy'):
696 self.progressbar = False
697 self.button_box = False
698 self.start_button = False
699 self.exit_button = False
701 # build the application's UI
705 self.box = gtk.VBox(False, 20)
706 self.window.add(self.box)
708 if 'error' in self.gui:
710 self.label = gtk.Label(self.gui_message)
711 self.label.set_line_wrap(True)
712 self.box.pack_start(self.label, True, True, 0)
716 self.button_box = gtk.HButtonBox()
717 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
718 self.box.pack_start(self.button_box, True, True, 0)
719 self.button_box.show()
721 if self.gui != 'error':
723 yes_image = gtk.Image()
724 yes_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
725 self.yes_button = gtk.Button("Yes")
726 self.yes_button.set_image(yes_image)
727 if self.gui == 'error_try_stable':
728 self.yes_button.connect("clicked", self.try_stable, None)
729 elif self.gui == 'error_try_default_mirror':
730 self.yes_button.connect("clicked", self.try_default_mirror, None)
731 elif self.gui == 'error_try_tor':
732 self.yes_button.connect("clicked", self.try_tor, None)
733 self.button_box.add(self.yes_button)
734 self.yes_button.show()
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()
745 elif self.gui == 'task':
747 self.label = gtk.Label(self.gui_message)
748 self.label.set_line_wrap(True)
749 self.box.pack_start(self.label, True, True, 0)
753 self.progressbar = gtk.ProgressBar(adjustment=None)
754 self.progressbar.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
755 self.progressbar.set_pulse_step(0.01)
756 self.box.pack_start(self.progressbar, True, True, 0)
759 self.button_box = gtk.HButtonBox()
760 self.button_box.set_layout(gtk.BUTTONBOX_SPREAD)
761 self.box.pack_start(self.button_box, True, True, 0)
762 self.button_box.show()
765 start_image = gtk.Image()
766 start_image.set_from_stock(gtk.STOCK_APPLY, gtk.ICON_SIZE_BUTTON)
767 self.start_button = gtk.Button(_("Start"))
768 self.start_button.set_image(start_image)
769 self.start_button.connect("clicked", self.start, None)
770 self.button_box.add(self.start_button)
771 if not self.gui_autostart:
772 self.start_button.show()
775 exit_image = gtk.Image()
776 exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
777 self.exit_button = gtk.Button(_("Exit"))
778 self.exit_button.set_image(exit_image)
779 self.exit_button.connect("clicked", self.destroy, None)
780 self.button_box.add(self.exit_button)
781 self.exit_button.show()
786 if self.gui_autostart:
789 # start button clicked, begin tasks
790 def start(self, widget, data=None):
791 # disable the start button
792 if self.start_button:
793 self.start_button.set_sensitive(False)
795 # start running tasks
798 # run the next task in the task list
802 if self.gui_task_i >= len(self.gui_tasks):
806 task = self.gui_tasks[self.gui_task_i]
808 # get ready for the next task
811 print _('Running task: {0}'.format(task))
812 if task == 'download_update_check':
813 print _('Downloading'), self.common.paths['update_check_url']
814 self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
816 if task == 'attempt_update':
817 print _('Checking to see if update is needed')
818 self.attempt_update()
820 elif task == 'download_sha256':
821 print _('Downloading'), self.common.paths['sha256_url'].format(self.common.settings['mirror'])
822 self.download('signature', self.common.paths['sha256_url'], self.common.paths['sha256_file'])
824 elif task == 'download_sha256_sig':
825 print _('Downloading'), self.common.paths['sha256_sig_url'].format(self.common.settings['mirror'])
826 self.download('signature', self.common.paths['sha256_sig_url'], self.common.paths['sha256_sig_file'])
828 elif task == 'download_tarball':
829 print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
830 self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
832 elif task == 'verify':
833 print _('Verifying signature')
836 elif task == 'extract':
837 print _('Extracting'), self.common.paths['tarball_filename']
841 print _('Running'), self.common.paths['tbb'][self.common.settings['preferred']]['start']
844 elif task == 'start_over':
845 print _('Starting download over again')
848 def response_received(self, response):
849 class FileDownloader(Protocol):
850 def __init__(self, common, file, total, progress, done_cb):
854 self.progress = progress
855 self.all_done = done_cb
857 if response.code != 200:
860 if response.code == 404:
861 if common.settings['preferred'] == 'alpha' and common.language != 'en-US':
865 raise TryStableException(_("It looks like the alpha version of Tor Browser Bundle isn't available for your language. Would you like to try the stable version instead?"))
867 if common.settings['mirror'] != common.default_mirror:
868 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']))
870 raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
872 def dataReceived(self, bytes):
873 self.file.write(bytes)
874 self.so_far += len(bytes)
875 percent = float(self.so_far) / float(self.total)
876 self.progress.set_fraction(percent)
877 amount = float(self.so_far)
879 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
882 amount = amount / float(size)
885 self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
887 def connectionLost(self, reason):
888 print _('Finished receiving body:'), reason.getErrorMessage()
889 self.all_done(reason)
891 dl = FileDownloader(self.common, self.file_download, response.length, self.progressbar, self.response_finished)
892 response.deliverBody(dl)
894 def response_finished(self, msg):
895 if msg.check(ResponseDone):
896 self.file_download.close()
897 delattr(self, 'current_download_path')
903 print "FINISHED", msg
904 ## FIXME handle errors
906 def download_error(self, f):
907 print _("Download error:"), f.value, type(f.value)
909 if isinstance(f.value, TryStableException):
910 f.trap(TryStableException)
911 self.set_gui('error_try_stable', str(f.value), [], False)
913 elif isinstance(f.value, TryDefaultMirrorException):
914 f.trap(TryDefaultMirrorException)
915 self.set_gui('error_try_default_mirror', str(f.value), [], False)
917 elif isinstance(f.value, DownloadErrorException):
918 f.trap(DownloadErrorException)
919 self.set_gui('error', str(f.value), [], False)
921 elif isinstance(f.value, DNSLookupError):
922 f.trap(DNSLookupError)
923 if common.settings['mirror'] != common.default_mirror:
924 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)
926 self.set_gui('error', str(f.value), [], False)
928 elif isinstance(f.value, ResponseFailed):
929 for reason in f.value.reasons:
930 if isinstance(reason.value, OpenSSL.SSL.Error):
931 # TODO: add the ability to report attack by posting bug to trac.torproject.org
932 if not self.common.settings['update_over_tor']:
933 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)
935 self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
938 self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
942 def download(self, name, url, path):
943 # keep track of current download
944 self.current_download_path = path
946 # initialize the progress bar
947 mirror_url = url.format(self.common.settings['mirror'])
948 self.progressbar.set_fraction(0)
949 self.progressbar.set_text(_('Downloading {0}').format(name))
950 self.progressbar.show()
953 if self.common.settings['update_over_tor']:
954 print _('Updating over Tor')
955 from twisted.internet.endpoints import TCP4ClientEndpoint
956 from txsocksx.http import SOCKS5Agent
958 torEndpoint = TCP4ClientEndpoint(reactor, '127.0.0.1', 9050)
960 # default mirror gets certificate pinning, only for requests that use the mirror
961 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
962 agent = SOCKS5Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']), proxyEndpoint=torEndpoint)
964 agent = SOCKS5Agent(reactor, proxyEndpoint=torEndpoint)
966 if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
967 agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
969 agent = Agent(reactor)
971 # actually, agent needs to follow redirect
972 agent = RedirectAgent(agent)
975 d = agent.request('GET', mirror_url,
976 Headers({'User-Agent': ['torbrowser-launcher']}),
979 self.file_download = open(path, 'w')
980 d.addCallback(self.response_received).addErrback(self.download_error)
982 if not reactor.running:
985 def try_stable(self, widget, data=None):
986 # change preferred to stable and relaunch TBL
987 self.common.settings['preferred'] = 'stable'
988 self.common.save_settings()
989 p = subprocess.Popen([self.common.paths['tbl_bin']])
992 def try_default_mirror(self, widget, data=None):
993 # change preferred to stable and relaunch TBL
994 self.common.settings['mirror'] = self.common.default_mirror
995 self.common.save_settings()
996 subprocess.Popen([self.common.paths['tbl_bin']])
999 def try_tor(self, widget, data=None):
1000 # set update_over_tor to true and relaunch TBL
1001 self.common.settings['update_over_tor'] = True
1002 self.common.save_settings()
1003 subprocess.Popen([self.common.paths['tbl_bin']])
1006 def attempt_update(self):
1007 # load the update check file
1009 versions = json.load(open(self.common.paths['update_check_file']))
1010 latest_stable = None
1013 # filter linux versions
1016 for version in versions:
1017 if '-Linux' in version:
1018 if 'alpha' in version or 'beta' in version or '-rc' in version:
1019 valid_alphas.append(str(version))
1021 valid_stables.append(str(version))
1023 if len(valid_alphas):
1024 latest_alpha = valid_alphas.pop()
1025 valid_stables.sort()
1026 if len(valid_stables):
1027 latest_stable = valid_stables.pop()
1029 if latest_stable or latest_alpha:
1031 self.common.settings['latest_version']['stable'] = latest_stable[:-len('-Linux')]
1033 self.common.settings['latest_version']['alpha'] = latest_alpha[:-len('-Linux')]
1034 self.common.settings['last_update_check_timestamp'] = int(time.time())
1035 self.common.settings['check_for_updates'] = False
1036 self.common.save_settings()
1037 self.common.build_paths(self.common.settings['latest_version'][self.common.settings['preferred']])
1038 self.start_launcher()
1041 # failed to find the latest version
1042 self.set_gui('error', _("Error checking for updates."), [], False)
1045 # not a valid JSON object
1046 self.set_gui('error', _("Error checking for updates."), [], False)
1053 # initialize the progress bar
1054 self.progressbar.set_fraction(0)
1055 self.progressbar.set_text(_('Verifying Signature'))
1056 self.progressbar.show()
1059 # check the sha256 file's sig, and also take the sha256 of the tarball and compare
1060 p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']])
1061 self.pulse_until_process_exits(p)
1062 if p.returncode == 0:
1063 # compare with sha256 of the tarball
1064 tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
1065 for line in open(self.common.paths['sha256_file'], 'r').readlines():
1066 if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
1072 # TODO: add the ability to report attack by posting bug to trac.torproject.org
1073 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)
1077 if not reactor.running:
1081 # initialize the progress bar
1082 self.progressbar.set_fraction(0)
1083 self.progressbar.set_text(_('Installing'))
1084 self.progressbar.show()
1089 if self.common.paths['tarball_file'][-2:] == 'xz':
1090 # if tarball is .tar.xz
1091 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
1092 tf = tarfile.open(fileobj=xz)
1093 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1096 # if tarball is .tar.gz
1097 if tarfile.is_tarfile(self.common.paths['tarball_file']):
1098 tf = tarfile.open(self.common.paths['tarball_file'])
1099 tf.extractall(self.common.paths['tbb'][self.common.settings['preferred']]['dir'])
1105 self.set_gui('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['start_over'], False)
1110 # installation is finished, so save installed_version
1111 self.common.settings['installed_version'] = self.common.settings['latest_version']
1112 self.common.save_settings()
1116 def run(self, run_next_task=True):
1117 subprocess.Popen([self.common.paths['tbb'][self.common.settings['preferred']]['start']])
1120 if self.common.settings['modem_sound']:
1124 sound = pygame.mixer.Sound(self.common.paths['modem_sound'])
1128 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."))
1129 md.set_position(gtk.WIN_POS_CENTER)
1137 # make the progress bar pulse until process p (a Popen object) finishes
1138 def pulse_until_process_exits(self, p):
1139 while p.poll() is None:
1141 self.progressbar.pulse()
1144 # start over and download TBB again
1145 def start_over(self):
1146 self.label.set_text(_("Downloading Tor Browser Bundle over again."))
1147 self.gui_tasks = ['download_tarball', 'verify', 'extract', 'run']
1152 def refresh_gtk(self):
1153 while gtk.events_pending():
1154 gtk.main_iteration(False)
1157 def delete_event(self, widget, event, data=None):
1160 def destroy(self, widget, data=None):
1161 if hasattr(self, 'file_download'):
1162 self.file_download.close()
1163 if hasattr(self, 'current_download_path'):
1164 os.remove(self.current_download_path)
1165 delattr(self, 'current_download_path')
1169 if __name__ == "__main__":
1170 tor_browser_launcher_version = open('/usr/share/torbrowser-launcher/version').read().strip()
1172 print _('Tor Browser Launcher')
1173 print _('By Micah Lee, licensed under GPLv3')
1174 print _('version {0}').format(tor_browser_launcher_version)
1175 print 'https://github.com/micahflee/torbrowser-launcher'
1177 common = TBLCommon(tor_browser_launcher_version)
1179 # is torbrowser-launcher already running?
1180 tbl_pid = common.get_pid(common.paths['tbl_bin'], True)
1182 print _('Tor Browser Launcher is already running (pid {0}), bringing to front').format(tbl_pid)
1183 common.bring_window_to_front(tbl_pid)
1186 if '-settings' in sys.argv:
1188 app = TBLSettings(common)
1192 app = TBLLauncher(common)