]> git.lizzy.rs Git - torbrowser-launcher.git/commitdiff
Add files via upload
authorCarl Joseph Hirner III <k.j.hirner.wisdom@gmail.com>
Sun, 29 Jul 2018 19:40:46 +0000 (12:40 -0700)
committerGitHub <noreply@github.com>
Sun, 29 Jul 2018 19:40:46 +0000 (12:40 -0700)
torbrowser_launcher/launcher.py [new file with mode: 0644]

diff --git a/torbrowser_launcher/launcher.py b/torbrowser_launcher/launcher.py
new file mode 100644 (file)
index 0000000..ee5ae14
--- /dev/null
@@ -0,0 +1,644 @@
+"""
+Tor Browser Launcher
+https://github.com/micahflee/torbrowser-launcher/
+
+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
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+"""
+
+import os
+import subprocess
+import time
+import tarfile
+import lzma
+import re
+import requests
+import gpg
+import shutil
+import xml.etree.ElementTree as ET
+
+from PyQt5 import QtCore, QtWidgets, QtGui
+
+
+class TryStableException(Exception):
+    pass
+
+
+class TryDefaultMirrorException(Exception):
+    pass
+
+
+class TryForcingEnglishException(Exception):
+    pass
+
+
+class DownloadErrorException(Exception):
+    pass
+
+
+class Launcher(QtWidgets.QMainWindow):
+    """
+    Launcher window.
+    """
+    def __init__(self, common, app, url_list):
+        super(Launcher, self).__init__()
+        self.common = common
+        self.app = app
+
+        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 = '7.5.2'
+
+        # Init launcher
+        self.set_state(None, '', [])
+        self.launch_gui = True
+
+        # If Tor Browser is not installed, detect latest version, download, and install
+        if not self.common.settings['installed'] or not self.check_min_version():
+            # 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 Tor Browser for the first time.")
+            elif not self.check_min_version():
+                download_message = _("Your version of Tor Browser is out-of-date. "
+                                     "Downloading the newest version.")
+
+            # Download and install
+            print(download_message)
+            self.set_state('task', download_message,
+                           ['download_version_check',
+                            'set_version',
+                            'download_sig',
+                            'download_tarball',
+                            'verify',
+                            'extract',
+                            'run'])
+
+            if self.common.settings['download_over_tor']:
+                print(_('Downloading over Tor'))
+
+        else:
+            # Tor Browser is already installed, so run
+            self.run(False)
+            self.launch_gui = False
+
+        if self.launch_gui:
+            # Build the rest of the UI
+
+            # Set up the window
+            self.setWindowTitle(_("Tor Browser"))
+            self.setWindowIcon(QtGui.QIcon(self.common.paths['icon_file']))
+
+            # Label
+            self.label = QtWidgets.QLabel()
+
+            # Progress bar
+            self.progress_bar = QtWidgets.QProgressBar()
+            self.progress_bar.setTextVisible(True)
+            self.progress_bar.setMinimum(0)
+            self.progress_bar.setMaximum(0)
+            self.progress_bar.setValue(0)
+
+            # Buttons
+            self.yes_button = QtWidgets.QPushButton()
+            self.yes_button.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_DialogApplyButton))
+            self.yes_button.clicked.connect(self.yes_clicked)
+            self.start_button = QtWidgets.QPushButton(_('Start'))
+            self.start_button.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_DialogApplyButton))
+            self.start_button.clicked.connect(self.start)
+            self.cancel_button = QtWidgets.QPushButton()
+            self.cancel_button.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_DialogCancelButton))
+            self.cancel_button.clicked.connect(self.close)
+            buttons_layout = QtWidgets.QHBoxLayout()
+            buttons_layout.addStretch()
+            buttons_layout.addWidget(self.yes_button)
+            buttons_layout.addWidget(self.start_button)
+            buttons_layout.addWidget(self.cancel_button)
+            buttons_layout.addStretch()
+
+            # Layout
+            layout = QtWidgets.QVBoxLayout()
+            layout.addWidget(self.label)
+            layout.addWidget(self.progress_bar)
+            layout.addLayout(buttons_layout)
+
+            central_widget = QtWidgets.QWidget()
+            central_widget.setLayout(layout)
+            self.setCentralWidget(central_widget)
+
+            self.update()
+
+    # Set the current state of Tor Browser Launcher
+    def set_state(self, gui, message, tasks, autostart=True):
+        self.gui = gui
+        self.gui_message = message
+        self.gui_tasks = tasks
+        self.gui_task_i = 0
+        self.gui_autostart = autostart
+
+    # Show and hide parts of the UI based on the current state
+    def update(self):
+        # Hide widgets
+        self.progress_bar.hide()
+        self.yes_button.hide()
+        self.start_button.hide()
+
+        if 'error' in self.gui:
+            # Label
+            self.label.setText(self.gui_message)
+
+            # Yes button
+            if self.gui != 'error':
+                self.yes_button.setText(_('Yes'))
+                self.yes_button.show()
+
+            # Exit button
+            self.cancel_button.setText(_('Exit'))
+
+        elif self.gui == 'task':
+            # Label
+            self.label.setText(self.gui_message)
+
+            # Progress bar
+            self.progress_bar.show()
+
+            # Start button
+            if not self.gui_autostart:
+                self.start_button.show()
+
+            # Cancel button
+            self.cancel_button.setText(_('Cancel'))
+
+        # Resize the window
+        self.adjustSize()
+
+        if self.gui_autostart:
+            self.start(None)
+
+    # Yes button clicked, based on the state decide what to do
+    def yes_clicked(self):
+        if self.gui == 'error_try_stable':
+            self.try_stable()
+        elif self.gui == 'error_try_default_mirror':
+            self.try_default_mirror()
+        elif self.gui == 'error_try_forcing_english':
+            self.try_forcing_english()
+        elif self.gui == 'error_try_tor':
+            self.try_tor()
+
+    # Start button clicked, begin tasks
+    def start(self, widget, data=None):
+        # Hide the start button
+        self.start_button.hide()
+
+        # Start running tasks
+        self.run_task()
+
+    # Run the next task in the task list
+    def run_task(self):
+        if self.gui_task_i >= len(self.gui_tasks):
+            self.close()
+            return
+
+        task = self.gui_tasks[self.gui_task_i]
+
+        # Get ready for the next task
+        self.gui_task_i += 1
+
+        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'])
+
+        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_state('error', _("Error detecting Tor Browser version."), [], False)
+                self.update()
+
+        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']))
+            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'))
+            self.verify()
+
+        elif task == 'extract':
+            print(_('Extracting'), self.common.paths['tarball_filename'])
+            self.extract()
+
+        elif task == 'run':
+            print(_('Running'), self.common.paths['tbb']['start'])
+            self.run()
+
+        elif task == 'start_over':
+            print(_('Starting download over again'))
+            self.start_over()
+
+    def download(self, name, url, path):
+        # Download from the selected mirror
+        mirror_url = url.format(self.common.settings['mirror']).encode()
+
+        # Initialize the progress bar
+        self.progress_bar.setValue(0)
+        self.progress_bar.setMaximum(100)
+        if self.common.settings['download_over_tor']:
+            self.progress_bar.setFormat(_('Downloading') + ' {0} '.format(name) + _('(over Tor)') + ', %p%')
+        else:
+            self.progress_bar.setFormat(_('Downloading') + ' {0}, %p%'.format(name))
+
+        def progress_update(total_bytes, bytes_so_far):
+            percent = float(bytes_so_far) / float(total_bytes)
+            amount = float(bytes_so_far)
+            units = "bytes"
+            for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
+                if amount > size:
+                    units = unit
+                    amount /= float(size)
+                    break
+
+            message = _('Downloaded') + (' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units))
+            if self.common.settings['download_over_tor']:
+                message += ' ' + _('(over Tor)')
+
+            self.progress_bar.setMaximum(total_bytes)
+            self.progress_bar.setValue(bytes_so_far)
+            self.progress_bar.setFormat(message)
+
+        def download_complete():
+            # Download complete, next task
+            self.run_task()
+
+        def download_error(gui, message):
+            print(message)
+            self.set_state(gui, message, [], False)
+            self.update()
+
+        t = DownloadThread(self.common, mirror_url, path)
+        t.progress_update.connect(progress_update)
+        t.download_complete.connect(download_complete)
+        t.download_error.connect(download_error)
+        t.start()
+        time.sleep(0.2)
+
+    def try_default_mirror(self):
+        # change mirror to default and relaunch TBL
+        self.common.settings['mirror'] = self.common.default_mirror
+        self.common.save_settings()
+        subprocess.Popen([self.common.paths['tbl_bin']])
+        self.close()
+
+    def try_forcing_english(self):
+        # 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.close()
+
+    def try_tor(self):
+        # 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.close()
+
+    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'])
+
+                # make sure the version does not contain directory traversal attempts
+                # e.g. "5.5.3", "6.0a", "6.0a-hardened" are valid but "../../../../.." is invalid
+                if not re.match(r'^[a-z0-9\.\-]+$', version):
+                    return None
+
+                return version
+        return None
+
+    def verify(self):
+        self.progress_bar.setValue(0)
+        self.progress_bar.setMaximum(0)
+        self.progress_bar.show()
+
+        self.label.setText(_('Verifying Signature'))
+
+        def success():
+            self.run_task()
+
+        def error(message):
+            # Make backup of tarball and sig
+            backup_tarball_filename = self.common.paths['tarball_file'] + '.verification_failed'
+            backup_sig_filename = self.common.paths['sig_file'] + '.verification_failed'
+            shutil.copyfile(self.common.paths['tarball_file'], backup_tarball_filename)
+            shutil.copyfile(self.common.paths['sig_file'], backup_sig_filename)
+
+            sigerror = 'SIGNATURE VERIFICATION FAILED!\n\n' \
+                       'Error Code: {0}\n\n' \
+                       'You might be under attack, there might be a network problem, or you may be missing a ' \
+                       'recently added Tor Browser verification key.\n\n' \
+                       'A copy of the Tor Browser files you downloaded have been saved here:\n' \
+                       '{1}\n{2}\n\n' \
+                       'Click Start to refresh the keyring and try again. If the message persists report the above ' \
+                       'error code here:\nhttps://github.com/micahflee/torbrowser-launcher/issues'
+            sigerror = sigerror.format(message, backup_tarball_filename, backup_sig_filename)
+
+            self.set_state('task', sigerror, ['start_over'], False)
+            self.update()
+
+        t = VerifyThread(self.common)
+        t.error.connect(error)
+        t.success.connect(success)
+        t.start()
+        time.sleep(0.2)
+
+    def extract(self):
+        self.progress_bar.setValue(0)
+        self.progress_bar.setMaximum(0)
+        self.progress_bar.show()
+
+        self.label.setText(_('Installing'))
+
+        def success():
+            self.run_task()
+
+        def error(message):
+            self.set_state(
+                'task',
+                _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])),
+                ['start_over'], False
+            )
+            self.update()
+
+        t = ExtractThread(self.common)
+        t.error.connect(error)
+        t.success.connect(success)
+        t.start()
+        time.sleep(0.2)
+
+    def check_min_version(self):
+        installed_version = None
+        print(self.common.paths['tbb']['changelog'])
+        for line in open(self.common.paths['tbb']['changelog'],'rb').readlines():
+            if line.startswith(b'Tor Browser '):
+                installed_version = line.split()[2].decode()
+                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)
+
+            Alert(self.common, message)
+            return
+
+        # Hide the TBL window (#151)
+        self.hide()
+
+        # Run Tor Browser
+        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.setText(_("Downloading Tor Browser over again."))
+        self.gui_tasks = ['download_tarball', 'verify', 'extract', 'run']
+        self.gui_task_i = 0
+        self.start(None)
+
+    def closeEvent(self, event):
+        # Clear the download cache
+        try:
+            os.remove(self.common.paths['version_check_file'])
+            os.remove(self.common.paths['sig_file'])
+            os.remove(self.common.paths['tarball_file'])
+        except:
+            pass
+
+        super(Launcher, self).closeEvent(event)
+
+
+class Alert(QtWidgets.QMessageBox):
+    """
+    An alert box dialog.
+    """
+    def __init__(self, common, message, icon=QtWidgets.QMessageBox.NoIcon, buttons=QtWidgets.QMessageBox.Ok, autostart=True):
+        super(Alert, self).__init__(None)
+
+        self.setWindowTitle(_("Tor Browser Launcher"))
+        self.setWindowIcon(QtGui.QIcon(common.paths['icon_file']))
+        self.setText(message)
+        self.setIcon(icon)
+        self.setStandardButtons(buttons)
+
+        if autostart:
+            self.exec_()
+
+
+class DownloadThread(QtCore.QThread):
+    """
+    Download a file in a separate thread.
+    """
+    progress_update = QtCore.pyqtSignal(int, int)
+    download_complete = QtCore.pyqtSignal()
+    download_error = QtCore.pyqtSignal(str, str)
+
+    def __init__(self, common, url, path):
+        super(DownloadThread, self).__init__()
+        self.common = common
+        self.url = url
+        self.path = path
+
+        # Use tor socks5 proxy, if enabled
+        if self.common.settings['download_over_tor']:
+            socks5_address = 'socks5://{}'.format(self.common.settings['tor_socks_address'])
+            self.proxies = {
+                'https': socks5_address,
+                'http': socks5_address
+            }
+        else:
+            self.proxies = None
+
+    def run(self):
+        with open(self.path, "wb") as f:
+            try:
+                # Start the request
+                r = requests.get(self.url,
+                                 headers={'User-Agent': 'torbrowser-launcher'},
+                                 stream=True, proxies=self.proxies)
+
+                # If status code isn't 200, something went wrong
+                if r.status_code != 200:
+                    # Should we use the default mirror?
+                    if self.common.settings['mirror'] != self.common.default_mirror:
+                        message = (_("Download Error:") +
+                                   " {0}\n\n" + _("You are currently using a non-default mirror") +
+                                   ":\n{1}\n\n" + _("Would you like to switch back to the default?")).format(
+                                       r.status_code, self.common.settings['mirror']
+                                   )
+                        self.download_error.emit('error_try_default_mirror', message)
+
+                    # Should we switch to English?
+                    elif self.common.language != 'en-US' and not self.common.settings['force_en-US']:
+                        message = (_("Download Error:") +
+                                   " {0}\n\n" +
+                                   _("Would you like to try the English version of Tor Browser instead?")).format(
+                                       r.status_code
+                                   )
+                        self.download_error.emit('error_try_forcing_english', message)
+
+                    else:
+                        message = (_("Download Error:") + " {0}").format(r.status_code)
+                        self.download_error.emit('error', message)
+
+                    r.close()
+                    return
+
+                # Start streaming the download
+                total_bytes = int(r.headers.get('content-length'))
+                bytes_so_far = 0
+                for data in r.iter_content(chunk_size=4096):
+                    bytes_so_far += len(data)
+                    f.write(data)
+                    self.progress_update.emit(total_bytes, bytes_so_far)
+
+            except requests.exceptions.SSLError:
+                message = _('Invalid SSL certificate for:\n{0}\n\nYou may be under attack.').format(self.url.decode())
+                if not self.common.settings['download_over_tor']:
+                    message += "\n\n" + _('Try the download again using Tor?')
+                    self.download_error.emit('error_try_tor', message)
+                else:
+                    self.download_error.emit('error', message)
+                return
+
+            except requests.exceptions.ConnectionError:
+                # Connection error
+                if self.common.settings['download_over_tor']:
+                    message = _("Error starting download:\n\n{0}\n\nTrying to download over Tor. "
+                                "Are you sure Tor is configured correctly and running?").format(self.url.decode())
+                    self.download_error.emit('error', message)
+                else:
+                    message = _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(
+                        self.url.decode()
+                    )
+                    self.download_error.emit('error', message)
+
+                return
+
+        self.download_complete.emit()
+
+
+class VerifyThread(QtCore.QThread):
+    """
+    Verify the signature in a separate thread
+    """
+    success = QtCore.pyqtSignal()
+    error = QtCore.pyqtSignal(str)
+
+    def __init__(self, common):
+        super(VerifyThread, self).__init__()
+        self.common = common
+
+    def run(self):
+        def verify(second_try=False):
+            with gpg.Context() as c:
+                c.set_engine_info(gpg.constants.protocol.OpenPGP, home_dir=self.common.paths['gnupg_homedir'])
+
+                sig = gpg.Data(file=self.common.paths['sig_file'])
+                signed = gpg.Data(file=self.common.paths['tarball_file'])
+
+                try:
+                    c.verify(signature=sig, signed_data=signed)
+                except gpg.errors.BadSignatures as e:
+                    if second_try:
+                        self.error.emit(str(e))
+                    else:
+                        raise Exception
+                else:
+                    self.success.emit()
+
+        try:
+            # Try verifying
+            verify()
+        except:
+            # If it fails, refresh the keyring and try again
+            self.common.refresh_keyring()
+            verify(True)
+
+
+class ExtractThread(QtCore.QThread):
+    """
+    Extract the tarball in a separate thread
+    """
+    success = QtCore.pyqtSignal()
+    error = QtCore.pyqtSignal()
+
+    def __init__(self, common):
+        super(ExtractThread, self).__init__()
+        self.common = common
+
+    def run(self):
+        extracted = False
+        try:
+            if self.common.paths['tarball_file'][-2:] == 'xz':
+                # if tarball is .tar.xz
+                xz = lzma.LZMAFile(self.common.paths['tarball_file'])
+                tf = tarfile.open(fileobj=xz)
+                tf.extractall(self.common.paths['tbb']['dir'])
+                extracted = True
+            else:
+                # if tarball is .tar.gz
+                if tarfile.is_tarfile(self.common.paths['tarball_file']):
+                    tf = tarfile.open(self.common.paths['tarball_file'])
+                    tf.extractall(self.common.paths['tbb']['dir'])
+                    extracted = True
+        except:
+            pass
+
+        if extracted:
+            self.success.emit()
+        else:
+            self.error.emit()