import re
import unicodedata
import requests
+import socks
import gpg
-import OpenSSL
import xml.etree.ElementTree as ET
from PyQt5 import QtCore, QtWidgets, QtGui
# 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:
- Alert(self.common, _("The python-txsocksx package is missing, downloads will not happen over tor"))
- 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']:
'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.start_button.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_DialogApplyButton))
self.start_button.clicked.connect(self.start)
self.cancel_button = QtWidgets.QPushButton()
- self.start_button.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_DialogCancelButton))
+ 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()
print(_('Starting download over again'))
self.start_over()
- def response_received(self, response):
- class FileDownloader(Protocol):
- def __init__(self, common, file, url, total, progress, done_cb):
- self.file = file
- self.total = total
- self.so_far = 0
- self.progress = progress
- self.all_done = done_cb
-
- if response.code != 200:
- if common.settings['mirror'] != common.default_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)
- )
-
- def dataReceived(self, bytes):
- self.file.write(bytes)
- self.so_far += len(bytes)
- percent = float(self.so_far) / float(self.total)
- self.progress.setValue(percent)
- amount = float(self.so_far)
- units = "bytes"
- for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
- if amount > size:
- units = unit
- amount /= float(size)
- break
-
- self.progress.setFormat(_('Downloaded')+(' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units)))
-
- def connectionLost(self, reason):
- self.all_done(reason)
-
- if hasattr(self, 'current_download_url'):
- url = self.current_download_url
- else:
- url = None
-
- dl = FileDownloader(
- self.common, self.file_download, url, response.length, self.progress_bar, self.response_finished
- )
- response.deliverBody(dl)
-
- def response_finished(self, msg):
- if msg.check(ResponseDone):
- self.file_download.close()
- delattr(self, 'current_download_path')
- delattr(self, 'current_download_url')
-
- # next task!
- self.run_task()
-
- else:
- print("FINISHED", msg)
- ## FIXME handle errors
-
- def download_error(self, f):
- print(_("Download Error:"), f.value, type(f.value))
-
- if isinstance(f.value, TryStableException):
- f.trap(TryStableException)
- self.set_state('error_try_stable', str(f.value), [], False)
-
- elif isinstance(f.value, TryDefaultMirrorException):
- f.trap(TryDefaultMirrorException)
- self.set_state('error_try_default_mirror', str(f.value), [], False)
-
- elif isinstance(f.value, TryForcingEnglishException):
- f.trap(TryForcingEnglishException)
- self.set_state('error_try_forcing_english', str(f.value), [], False)
-
- elif isinstance(f.value, DownloadErrorException):
- f.trap(DownloadErrorException)
- self.set_state('error', str(f.value), [], False)
-
- elif isinstance(f.value, DNSLookupError):
- f.trap(DNSLookupError)
- if common.settings['mirror'] != common.default_mirror:
- self.set_state('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_state('error', str(f.value), [], False)
-
- elif isinstance(f.value, ResponseFailed):
- 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['download_over_tor']:
- self.set_state('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_state('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_state('error', _("Error connecting to Tor at {0}").format(addr), [], False)
-
- else:
- self.set_state('error', _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(f.value), [], False)
-
- self.update()
-
def download(self, name, url, path):
- # Keep track of current download
- self.current_download_path = path
- self.current_download_url = url.encode()
-
- mirror_url = url.format(self.common.settings['mirror'])
- mirror_url = mirror_url.encode()
+ # 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)
- self.progress_bar.setFormat(_('Downloading') + ' {0}, %p%'.format(name))
-
if self.common.settings['download_over_tor']:
- # TODO: make requests work over SOCKS5 proxy
- # this is the proxy to use: self.common.settings['tor_socks_address']
- pass
+ 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)')
- with open(self.current_download_path, "wb") as f:
- # Start the request
- r = requests.get(mirror_url, headers={'User-Agent': 'torbrowser-launcher'}, stream=True)
- total_length = r.headers.get('content-length')
+ self.progress_bar.setMaximum(total_bytes)
+ self.progress_bar.setValue(bytes_so_far)
+ self.progress_bar.setFormat(message)
- if total_length is None: # no content length header
- f.write(r.content)
- else:
- dl = 0
- total_length = int(total_length)
- for data in r.iter_content(chunk_size=4096):
- dl += len(data)
- f.write(data)
- done = int(50 * dl / total_length)
- print('{} / {}'.format(dl, total_length), end='\r')
+ def download_complete():
+ # Download complete, next task
+ self.run_task()
- # 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, widget, data=None):
+ 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, widget, data=None):
+ 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, widget, data=None):
+ def try_tor(self):
# set download_over_tor to true and relaunch TBL
self.common.settings['download_over_tor'] = True
self.common.save_settings()
def verify(self):
self.progress_bar.setValue(0)
self.progress_bar.setMaximum(0)
- self.progress_bar.setFormat(_('Verifying Signature'))
self.progress_bar.show()
- def gui_raise_sigerror(self, sigerror='MissingErr'):
- """
- :type sigerror: str
- """
+ self.label.setText(_('Verifying Signature'))
+
+ def success():
+ self.run_task()
+
+ def error(message):
sigerror = 'SIGNATURE VERIFICATION FAILED!\n\nError Code: {0}\n\nYou might be under attack, there might' \
' be a network\nproblem, or you may be missing a recently added\nTor Browser verification key.' \
'\nClick Start to refresh the keyring and try again. If the message persists report the above' \
self.set_state('task', sigerror, ['start_over'], False)
self.update()
- 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:
- result = str(e).split(": ")
- if result[1] == 'Bad signature':
- gui_raise_sigerror(self, str(e))
- elif result[1] == 'No public key':
- self.common.refresh_keyring(result[0])
- gui_raise_sigerror(self, str(e))
- else:
- self.run_task()
+ t = VerifyThread(self.common)
+ t.error.connect(error)
+ t.success.connect(success)
+ t.start()
+ time.sleep(0.2)
def extract(self):
- # initialize the progress bar
self.progress_bar.setValue(0)
self.progress_bar.setMaximum(0)
- self.progress_bar.setFormat(_('Installing'))
self.progress_bar.show()
- 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
+ self.label.setText(_('Installing'))
- if not extracted:
+ 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()
- return
- self.run_task()
+ 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
self.start(None)
def closeEvent(self, event):
- if hasattr(self, 'file_download'):
- self.file_download.close()
- if hasattr(self, 'current_download_path'):
- os.remove(self.current_download_path)
- delattr(self, 'current_download_path')
- delattr(self, 'current_download_url')
+ # 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)
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:
+ if not self.common.settings['download_over_tor']:
+ message = _('Invalid SSL certificate for:\n{0}\n\nYou may be under attack.').format(self.url.decode()) + "\n\n" + _('Try the download again using Tor?')
+ self.download_error.emit('error_try_tor', message)
+ else:
+ message = _('Invalid SSL certificate for:\n{0}\n\nYou may be under attack.'.format(self.url.decode()))
+ self.download_error.emit('error', message)
+ return
+
+ except requests.exceptions.ConnectionError:
+ # Connection error
+ message = _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(self.url.decode())
+ self.download_error.emit('error', message)
+ # TODO: check for SSL error, also check if connecting over Tor if there's a socks5 error
+ 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):
+ 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:
+ result = str(e).split(": ")
+ if result[1] == 'No public key':
+ self.common.refresh_keyring(result[0])
+ self.error.emit(str(e))
+ else:
+ self.success.emit()
+
+
+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()