Tor Browser Launcher
https://github.com/micahflee/torbrowser-launcher/
-Copyright (c) 2013-2014 Micah Lee <micah@micahflee.com>
+Copyright (c) 2013-2017 Micah Lee <micah@micahflee.com>
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
OTHER DEALINGS IN THE SOFTWARE.
"""
-import os, subprocess, time, json, tarfile, hashlib, lzma, threading, re
+import os, subprocess, time, json, tarfile, hashlib, lzma, threading, re, unicodedata
from twisted.internet import reactor
from twisted.web.client import Agent, RedirectAgent, ResponseDone, ResponseFailed
from twisted.web.http_headers import Headers
from twisted.internet.protocol import Protocol
-from twisted.internet.ssl import ClientContextFactory
-from twisted.internet.error import DNSLookupError
+from twisted.internet.error import DNSLookupError, ConnectionRefusedError
+
+import xml.etree.ElementTree as ET
import OpenSSL
class TryDefaultMirrorException(Exception):
pass
-class DownloadErrorException(Exception):
+class TryForcingEnglishException(Exception):
pass
-class VerifyTorProjectCert(ClientContextFactory):
- def __init__(self, torproject_pem):
- self.torproject_ca = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, open(torproject_pem, 'r').read())
-
- def getContext(self, host, port):
- ctx = ClientContextFactory.getContext(self)
- ctx.set_verify_depth(0)
- ctx.set_verify(OpenSSL.SSL.VERIFY_PEER | OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, self.verifyHostname)
- return ctx
-
- def verifyHostname(self, connection, cert, errno, depth, preverifyOK):
- return cert.digest('sha256') == self.torproject_ca.digest('sha256')
+class DownloadErrorException(Exception):
+ pass
class Launcher:
def __init__(self, common, url_list):
- print _('Starting launcher dialog')
self.common = common
self.url_list = url_list
+ self.force_redownload = False
+
+ # this is the current version of Tor Browser, which should get updated with every release
+ self.min_version = '6.0.2'
# init launcher
self.set_gui(None, '', [])
self.launch_gui = True
- print "LATEST VERSION", self.common.settings['latest_version']
- self.common.build_paths(self.common.settings['latest_version'])
-
- if self.common.settings['update_over_tor']:
- try:
- import txsocksx
- except ImportError:
- 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"))
- md.set_position(gtk.WIN_POS_CENTER)
- md.run()
- md.destroy()
- self.common.settings['update_over_tor'] = False
- self.common.save_settings()
-
- # is firefox already running?
- if self.common.settings['installed_version']:
- firefox_pid = self.common.get_pid('./Browser/firefox')
- if firefox_pid:
- print _('Firefox is open, bringing to focus')
- # bring firefox to front
- self.common.bring_window_to_front(firefox_pid)
- return
-
- # check for updates?
- check_for_updates = False
- if self.common.settings['check_for_updates']:
- check_for_updates = True
-
- if not check_for_updates:
- # how long was it since the last update check?
- # 86400 seconds = 24 hours
- current_timestamp = int(time.time())
- if current_timestamp - self.common.settings['last_update_check_timestamp'] >= 86400:
- check_for_updates = True
-
- if check_for_updates:
- # check for update
- print 'Checking for update'
- self.set_gui('task', _("Checking for Tor Browser update."),
- ['download_update_check',
- 'attempt_update'])
+
+ # if Tor Browser is not installed, detect latest version, download, and install
+ if not self.common.settings['installed'] or not self.check_min_version():
+ # if downloading over Tor, include txsocksx
+ if self.common.settings['download_over_tor']:
+ try:
+ import txsocksx
+ print _('Downloading over Tor')
+ except ImportError:
+ 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"))
+ md.set_position(gtk.WIN_POS_CENTER)
+ md.run()
+ md.destroy()
+ self.common.settings['download_over_tor'] = False
+ self.common.save_settings()
+
+ # different message if downloading for the first time, or because your installed version is too low
+ download_message = ""
+ if not self.common.settings['installed']:
+ download_message = _("Downloading and installing Tor Browser for the first time.")
+ elif not self.check_min_version():
+ download_message = _("Your version of Tor Browser is out-of-date. Downloading and installing the newest version.")
+
+ # download and install
+ print download_message
+ self.set_gui('task', download_message,
+ ['download_version_check',
+ 'set_version',
+ 'download_sig',
+ 'download_tarball',
+ 'verify',
+ 'extract',
+ 'run'])
+
else:
- # no need to check for update
- print _('Checked for update within 24 hours, skipping')
- self.start_launcher()
+ # Tor Browser is already installed, so run
+ self.run(False)
+ self.launch_gui = False
if self.launch_gui:
- # set up the window
+ # build the rest of the UI
+ self.build_ui()
+
+ def configure_window(self):
+ if not hasattr(self, 'window'):
self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
self.window.set_title(_("Tor Browser"))
self.window.set_icon_from_file(self.common.paths['icon_file'])
self.window.connect("delete_event", self.delete_event)
self.window.connect("destroy", self.destroy)
- # build the rest of the UI
- self.build_ui()
-
- # download or run TBB
- def start_launcher(self):
- # is TBB already installed?
- latest_version = self.common.settings['latest_version']
- installed_version = self.common.settings['installed_version']
-
- # verify installed version for newer versions of TBB (#58)
- if installed_version >= '3.0':
- versions_filename = self.common.paths['tbb']['versions']
- if os.path.exists(versions_filename):
- for line in open(versions_filename):
- if 'TORBROWSER_VERSION' in line:
- installed_version = line.lstrip('TORBROWSER_VERSION=').strip()
-
- start = self.common.paths['tbb']['start']
- if os.path.isfile(start) and os.access(start, os.X_OK):
- if installed_version == latest_version:
- print _('Latest version of TBB is installed, launching')
- # current version of tbb is installed, launch it
- self.run(False)
- self.launch_gui = False
- elif installed_version < latest_version:
- print _('TBB is out of date, attempting to upgrade to {0}'.format(latest_version))
- # there is a tbb upgrade available
- self.set_gui('task', _("Your Tor Browser is out of date. Upgrading from {0} to {1}.".format(installed_version, latest_version)),
- ['download_sha256',
- 'download_sha256_sig',
- 'download_tarball',
- 'verify',
- 'extract',
- 'run'])
- else:
- # for some reason the installed tbb is newer than the current version?
- self.set_gui('error', _("Something is wrong. The version of Tor Browser Bundle you have installed is newer than the current version?"), [])
-
- # not installed
- else:
- print _('TBB is not installed, attempting to install {0}'.format(latest_version))
- self.set_gui('task', _("Downloading and installing Tor Browser for the first time."),
- ['download_sha256',
- 'download_sha256_sig',
- 'download_tarball',
- 'verify',
- 'extract',
- 'run'])
-
# there are different GUIs that might appear, this sets which one we want
def set_gui(self, gui, message, tasks, autostart=True):
self.gui = gui
self.clear_ui()
self.box = gtk.VBox(False, 20)
+ self.configure_window()
self.window.add(self.box)
if 'error' in self.gui:
self.yes_button.connect("clicked", self.try_stable, None)
elif self.gui == 'error_try_default_mirror':
self.yes_button.connect("clicked", self.try_default_mirror, None)
+ elif self.gui == 'error_try_forcing_english':
+ self.yes_button.connect("clicked", self.try_forcing_english, None)
elif self.gui == 'error_try_tor':
self.yes_button.connect("clicked", self.try_tor, None)
self.button_box.add(self.yes_button)
# exit button
exit_image = gtk.Image()
exit_image.set_from_stock(gtk.STOCK_CANCEL, gtk.ICON_SIZE_BUTTON)
- self.exit_button = gtk.Button(_("Exit"))
+ self.exit_button = gtk.Button(_("Cancel"))
self.exit_button.set_image(exit_image)
self.exit_button.connect("clicked", self.destroy, None)
self.button_box.add(self.exit_button)
# get ready for the next task
self.gui_task_i += 1
- print _('Running task: {0}'.format(task))
- if task == 'download_update_check':
- print _('Downloading'), self.common.paths['update_check_url']
- self.download('update check', self.common.paths['update_check_url'], self.common.paths['update_check_file'])
-
- if task == 'attempt_update':
- print _('Checking to see if update is needed')
- self.attempt_update()
+ if task == 'download_version_check':
+ print _('Downloading'), self.common.paths['version_check_url']
+ self.download('version check', self.common.paths['version_check_url'], self.common.paths['version_check_file'])
- elif task == 'download_sha256':
- print _('Downloading'), self.common.paths['sha256_url'].format(self.common.settings['mirror'])
- self.download('signature', self.common.paths['sha256_url'], self.common.paths['sha256_file'])
+ if task == 'set_version':
+ version = self.get_stable_version()
+ if version:
+ self.common.build_paths(self.get_stable_version())
+ print _('Latest version: {}').format(version)
+ self.run_task()
+ else:
+ self.set_gui('error', _("Error detecting Tor Browser version."), [], False)
+ self.clear_ui()
+ self.build_ui()
- elif task == 'download_sha256_sig':
- print _('Downloading'), self.common.paths['sha256_sig_url'].format(self.common.settings['mirror'])
- self.download('signature', self.common.paths['sha256_sig_url'], self.common.paths['sha256_sig_file'])
+ elif task == 'download_sig':
+ print _('Downloading'), self.common.paths['sig_url'].format(self.common.settings['mirror'])
+ self.download('signature', self.common.paths['sig_url'], self.common.paths['sig_file'])
elif task == 'download_tarball':
print _('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror'])
- self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
+ if not self.force_redownload and os.path.exists(self.common.paths['tarball_file']):
+ self.run_task()
+ else:
+ self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
elif task == 'verify':
- print _('Verifying signature')
+ print _('Verifying Signature')
self.verify()
elif task == 'extract':
if response.code != 200:
if common.settings['mirror'] != common.default_mirror:
- 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']))
+ raise TryDefaultMirrorException((_("Download Error:") + " {0} {1}\n\n" + _("You are currently using a non-default mirror") + ":\n{2}\n\n" + _("Would you like to switch back to the default?")).format(response.code, response.phrase, common.settings['mirror']))
+ elif common.language != 'en-US' and not common.settings['force_en-US']:
+ raise TryForcingEnglishException((_("Download Error:") + " {0} {1}\n\n" + _("Would you like to try the English version of Tor Browser instead?")).format(response.code, response.phrase))
else:
- raise DownloadErrorException(_("Download Error: {0} {1}").format(response.code, response.phrase))
+ raise DownloadErrorException((_("Download Error:") + " {0} {1}").format(response.code, response.phrase))
def dataReceived(self, bytes):
self.file.write(bytes)
self.progress.set_text(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
def connectionLost(self, reason):
- print _('Finished receiving body:'), reason.getErrorMessage()
self.all_done(reason)
if hasattr(self, 'current_download_url'):
## FIXME handle errors
def download_error(self, f):
- print _("Download error:"), f.value, type(f.value)
+ print _("Download Error:"), f.value, type(f.value)
if isinstance(f.value, TryStableException):
f.trap(TryStableException)
f.trap(TryDefaultMirrorException)
self.set_gui('error_try_default_mirror', str(f.value), [], False)
+ elif isinstance(f.value, TryForcingEnglishException):
+ f.trap(TryForcingEnglishException)
+ self.set_gui('error_try_forcing_english', str(f.value), [], False)
+
elif isinstance(f.value, DownloadErrorException):
f.trap(DownloadErrorException)
self.set_gui('error', str(f.value), [], False)
elif isinstance(f.value, DNSLookupError):
f.trap(DNSLookupError)
if common.settings['mirror'] != common.default_mirror:
- 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)
+ self.set_gui('error_try_default_mirror', (_("DNS Lookup Error") + "\n\n" + _("You are currently using a non-default mirror") + ":\n{0}\n\n" + _("Would you like to switch back to the default?")).format(common.settings['mirror']), [], False)
else:
self.set_gui('error', str(f.value), [], False)
for reason in f.value.reasons:
if isinstance(reason.value, OpenSSL.SSL.Error):
# TODO: add the ability to report attack by posting bug to trac.torproject.org
- if not self.common.settings['update_over_tor']:
- 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)
+ if not self.common.settings['download_over_tor']:
+ 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)
else:
self.set_gui('error', _('The SSL certificate served by https://www.torproject.org is invalid! You may be under attack.'), [], False)
+ elif isinstance(f.value, ConnectionRefusedError) and self.common.settings['download_over_tor']:
+ # If we're using Tor, we'll only get this error when we fail to
+ # connect to the SOCKS server. If the connection fails at the
+ # remote end, we'll get txsocksx.errors.ConnectionRefused.
+ addr = self.common.settings['tor_socks_address']
+ self.set_gui('error', _("Error connecting to Tor at {0}").format(addr), [], False)
+
else:
self.set_gui('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
self.current_download_path = path
self.current_download_url = url
- # initialize the progress bar
mirror_url = url.format(self.common.settings['mirror'])
+
+ # convert mirror_url from unicode to string, if needed (#205)
+ if isinstance(mirror_url, unicode):
+ mirror_url = unicodedata.normalize('NFKD', mirror_url).encode('ascii','ignore')
+
+ # initialize the progress bar
self.progressbar.set_fraction(0)
- self.progressbar.set_text(_('Downloading {0}').format(name))
+ self.progressbar.set_text(_('Downloading') + ' {0}'.format(name))
self.progressbar.show()
self.refresh_gtk()
- if self.common.settings['update_over_tor']:
- print _('Updating over Tor')
- from twisted.internet.endpoints import TCP4ClientEndpoint
+ if self.common.settings['download_over_tor']:
+ from twisted.internet.endpoints import clientFromString
from txsocksx.http import SOCKS5Agent
- torEndpoint = TCP4ClientEndpoint(reactor, '127.0.0.1', 9050)
+ torEndpoint = clientFromString(reactor, self.common.settings['tor_socks_address'])
# default mirror gets certificate pinning, only for requests that use the mirror
- if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
- agent = SOCKS5Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']), proxyEndpoint=torEndpoint)
- else:
- agent = SOCKS5Agent(reactor, proxyEndpoint=torEndpoint)
+ agent = SOCKS5Agent(reactor, proxyEndpoint=torEndpoint)
else:
- if self.common.settings['mirror'] == self.common.default_mirror and '{0}' in url:
- agent = Agent(reactor, VerifyTorProjectCert(self.common.paths['torproject_pem']))
- else:
- agent = Agent(reactor)
+ agent = Agent(reactor)
# actually, agent needs to follow redirect
agent = RedirectAgent(agent)
subprocess.Popen([self.common.paths['tbl_bin']])
self.destroy(False)
+ def try_forcing_english(self, widget, data=None):
+ # change force english to true and relaunch TBL
+ self.common.settings['force_en-US'] = True
+ self.common.save_settings()
+ subprocess.Popen([self.common.paths['tbl_bin']])
+ self.destroy(False)
+
def try_tor(self, widget, data=None):
- # set update_over_tor to true and relaunch TBL
- self.common.settings['update_over_tor'] = True
+ # set download_over_tor to true and relaunch TBL
+ self.common.settings['download_over_tor'] = True
self.common.save_settings()
subprocess.Popen([self.common.paths['tbl_bin']])
self.destroy(False)
- def attempt_update(self):
- # load the update check file
- try:
- versions = json.load(open(self.common.paths['update_check_file']))
- latest = None
-
- # filter linux versions
- valid = []
- for version in versions:
- if '-Linux' in version:
- valid.append(str(version))
- valid.sort()
- if len(valid):
- versions = valid
-
- if len(versions) == 1:
- latest = versions.pop()
- else:
- stable = []
- # remove alphas/betas
- for version in versions:
- if not re.search(r'a\d-Linux', version) and not re.search(r'b\d-Linux', version):
- stable.append(version)
- if len(stable):
- latest = stable.pop()
- else:
- latest = versions.pop()
-
- if latest:
- latest = str(latest)
- if latest.endswith('-Linux'):
- latest = latest.rstrip('-Linux')
-
- self.common.settings['latest_version'] = latest
- self.common.settings['last_update_check_timestamp'] = int(time.time())
- self.common.settings['check_for_updates'] = False
- self.common.save_settings()
- self.common.build_paths(self.common.settings['latest_version'])
- self.start_launcher()
+ def get_stable_version(self):
+ tree = ET.parse(self.common.paths['version_check_file'])
+ for up in tree.getroot():
+ if up.tag == 'update' and up.attrib['appVersion']:
+ version = str(up.attrib['appVersion'])
- else:
- # failed to find the latest version
- self.set_gui('error', _("Error checking for updates."), [], False)
+ # make sure the version does not contain directory traversal attempts
+ # e.g. "5.5.3", "6.0a", "6.0a-hardned" are valid but "../../../../.." is invalid
+ if not re.match(r'^[a-z0-9\.\-]+$', version):
+ return None
- except:
- # not a valid JSON object
- self.set_gui('error', _("Error checking for updates."), [], False)
-
- # now start over
- self.clear_ui()
- self.build_ui()
+ return version
+ return None
def verify(self):
# initialize the progress bar
self.progressbar.set_text(_('Verifying Signature'))
self.progressbar.show()
+ # verify the PGP signature
verified = False
- # check the sha256 file's sig, and also take the sha256 of the tarball and compare
- p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sha256_sig_file']])
+ FNULL = open(os.devnull, 'w')
+ p = subprocess.Popen(['/usr/bin/gpg', '--homedir', self.common.paths['gnupg_homedir'], '--verify', self.common.paths['sig_file'], self.common.paths['tarball_file']], stdout=FNULL, stderr=subprocess.STDOUT)
self.pulse_until_process_exits(p)
if p.returncode == 0:
- # compare with sha256 of the tarball
- tarball_sha256 = hashlib.sha256(open(self.common.paths['tarball_file'], 'r').read()).hexdigest()
- for line in open(self.common.paths['sha256_file'], 'r').readlines():
- if tarball_sha256.lower() in line.lower() and self.common.paths['tarball_filename'] in line:
- verified = True
+ verified = True
if verified:
self.run_task()
self.build_ui()
return
- # installation is finished, so save installed_version
- self.common.settings['installed_version'] = self.common.settings['latest_version']
- self.common.save_settings()
-
self.run_task()
+ def check_min_version(self):
+ installed_version = None
+ for line in open(self.common.paths['tbb']['versions']).readlines():
+ if line.startswith('TORBROWSER_VERSION='):
+ installed_version = line.split('=')[1].strip()
+ break
+
+ if self.min_version <= installed_version:
+ return True
+
+ return False
+
def run(self, run_next_task=True):
+ # don't run if it isn't at least the minimum version
+ if not self.check_min_version():
+ message = _("The version of Tor Browser you have installed is earlier than it should be, which could be a sign of an attack!")
+ print message
+
+ md = gtk.MessageDialog(None, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_WARNING, gtk.BUTTONS_CLOSE, _(message))
+ md.set_position(gtk.WIN_POS_CENTER)
+ md.run()
+ md.destroy()
+
+ return
+
# play modem sound?
if self.common.settings['modem_sound']:
def play_modem_sound():
gtk.main_iteration_do(True)
# run Tor Browser
- if self.common.settings['accept_links']:
- subprocess.call([self.common.paths['tbb']['start'], '-allow-remote'] + self.url_list)
- else:
- subprocess.call([self.common.paths['tbb']['start']])
+ subprocess.call([self.common.paths['tbb']['start']], cwd=self.common.paths['tbb']['dir_tbb'])
if run_next_task:
self.run_task()
# start over and download TBB again
def start_over(self):
+ self.force_redownload = True # Overwrite any existing file
self.label.set_text(_("Downloading Tor Browser Bundle over again."))
self.gui_tasks = ['download_tarball', 'verify', 'extract', 'run']
self.gui_task_i = 0
delattr(self, 'current_download_url')
if reactor.running:
reactor.stop()
-