From 4e4bbd672266347f02dbd3f5f3796db0d9d4c323 Mon Sep 17 00:00:00 2001 From: Carl Joseph Hirner III Date: Sun, 29 Jul 2018 12:40:21 -0700 Subject: [PATCH 1/2] Delete launcher.py --- torbrowser_launcher/launcher.py | 643 -------------------------------- 1 file changed, 643 deletions(-) delete mode 100644 torbrowser_launcher/launcher.py diff --git a/torbrowser_launcher/launcher.py b/torbrowser_launcher/launcher.py deleted file mode 100644 index 55ca503..0000000 --- a/torbrowser_launcher/launcher.py +++ /dev/null @@ -1,643 +0,0 @@ -""" -Tor Browser Launcher -https://github.com/micahflee/torbrowser-launcher/ - -Copyright (c) 2013-2017 Micah Lee - -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 - for line in open(self.common.paths['tbb']['changelog']).readlines(): - if line.startswith('Tor Browser '): - installed_version = line.split()[2] - 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() From bd9e67c293411924485d73b3505aedcd71171f79 Mon Sep 17 00:00:00 2001 From: Carl Joseph Hirner III Date: Sun, 29 Jul 2018 12:40:46 -0700 Subject: [PATCH 2/2] Add files via upload --- torbrowser_launcher/launcher.py | 644 ++++++++++++++++++++++++++++++++ 1 file changed, 644 insertions(+) create mode 100644 torbrowser_launcher/launcher.py diff --git a/torbrowser_launcher/launcher.py b/torbrowser_launcher/launcher.py new file mode 100644 index 0000000..ee5ae14 --- /dev/null +++ b/torbrowser_launcher/launcher.py @@ -0,0 +1,644 @@ +""" +Tor Browser Launcher +https://github.com/micahflee/torbrowser-launcher/ + +Copyright (c) 2013-2017 Micah Lee + +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()