3 https://github.com/micahflee/torbrowser-launcher/
5 Copyright (c) 2013-2017 Micah Lee <micah@micahflee.com>
7 Permission is hereby granted, free of charge, to any person
8 obtaining a copy of this software and associated documentation
9 files (the "Software"), to deal in the Software without
10 restriction, including without limitation the rights to use,
11 copy, modify, merge, publish, distribute, sublicense, and/or sell
12 copies of the Software, and to permit persons to whom the
13 Software is furnished to do so, subject to the following
16 The above copyright notice and this permission notice shall be
17 included in all copies or substantial portions of the Software.
19 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
21 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
23 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
24 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26 OTHER DEALINGS IN THE SOFTWARE.
37 import xml.etree.ElementTree as ET
39 from PyQt5 import QtCore, QtWidgets, QtGui
42 class TryStableException(Exception):
46 class TryDefaultMirrorException(Exception):
50 class TryForcingEnglishException(Exception):
54 class DownloadErrorException(Exception):
58 class Launcher(QtWidgets.QMainWindow):
62 def __init__(self, common, app, url_list):
63 super(Launcher, self).__init__()
67 self.url_list = url_list
68 self.force_redownload = False
70 # This is the current version of Tor Browser, which should get updated with every release
71 self.min_version = '7.5.2'
74 self.set_state(None, '', [])
75 self.launch_gui = True
77 # If Tor Browser is not installed, detect latest version, download, and install
78 if not self.common.settings['installed'] or not self.check_min_version():
79 # Different message if downloading for the first time, or because your installed version is too low
81 if not self.common.settings['installed']:
82 download_message = _("Downloading and installing Tor Browser for the first time.")
83 elif not self.check_min_version():
84 download_message = _("Your version of Tor Browser is out-of-date. "
85 "Downloading and installing the newest version.")
87 # Download and install
88 print(download_message)
89 self.set_state('task', download_message,
90 ['download_version_check',
98 if self.common.settings['download_over_tor']:
99 print(_('Downloading over Tor'))
102 # Tor Browser is already installed, so run
104 self.launch_gui = False
107 # Build the rest of the UI
110 self.setWindowTitle(_("Tor Browser"))
111 self.setWindowIcon(QtGui.QIcon(self.common.paths['icon_file']))
114 self.label = QtWidgets.QLabel()
117 self.progress_bar = QtWidgets.QProgressBar()
118 self.progress_bar.setTextVisible(True)
119 self.progress_bar.setMinimum(0)
120 self.progress_bar.setMaximum(0)
121 self.progress_bar.setValue(0)
124 self.yes_button = QtWidgets.QPushButton()
125 self.yes_button.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_DialogApplyButton))
126 self.yes_button.clicked.connect(self.yes_clicked)
127 self.start_button = QtWidgets.QPushButton()
128 self.start_button.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_DialogApplyButton))
129 self.start_button.clicked.connect(self.start)
130 self.cancel_button = QtWidgets.QPushButton()
131 self.cancel_button.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_DialogCancelButton))
132 self.cancel_button.clicked.connect(self.close)
133 buttons_layout = QtWidgets.QHBoxLayout()
134 buttons_layout.addStretch()
135 buttons_layout.addWidget(self.yes_button)
136 buttons_layout.addWidget(self.start_button)
137 buttons_layout.addWidget(self.cancel_button)
138 buttons_layout.addStretch()
141 layout = QtWidgets.QVBoxLayout()
142 layout.addWidget(self.label)
143 layout.addWidget(self.progress_bar)
144 layout.addLayout(buttons_layout)
146 central_widget = QtWidgets.QWidget()
147 central_widget.setLayout(layout)
148 self.setCentralWidget(central_widget)
153 # Set the current state of Tor Browser Launcher
154 def set_state(self, gui, message, tasks, autostart=True):
156 self.gui_message = message
157 self.gui_tasks = tasks
159 self.gui_autostart = autostart
161 # Show and hide parts of the UI based on the current state
164 self.progress_bar.hide()
165 self.yes_button.hide()
166 self.start_button.hide()
168 if 'error' in self.gui:
170 self.label.setText(self.gui_message)
173 if self.gui != 'error':
174 self.yes_button.setText(_('Yes'))
175 self.yes_button.show()
178 self.cancel_button.setText(_('Exit'))
180 elif self.gui == 'task':
182 self.label.setText(self.gui_message)
185 self.progress_bar.show()
188 if not self.gui_autostart:
189 self.start_button.show()
192 self.cancel_button.setText(_('Cancel'))
194 if self.gui_autostart:
197 # Yes button clicked, based on the state decide what to do
198 def yes_clicked(self):
199 if self.gui == 'error_try_stable':
201 elif self.gui == 'error_try_default_mirror':
202 self.try_default_mirror()
203 elif self.gui == 'error_try_forcing_english':
204 self.try_forcing_english()
205 elif self.gui == 'error_try_tor':
208 # Start button clicked, begin tasks
209 def start(self, widget, data=None):
210 # Hide the start button
211 self.start_button.hide()
213 # Start running tasks
216 # Run the next task in the task list
218 if self.gui_task_i >= len(self.gui_tasks):
222 task = self.gui_tasks[self.gui_task_i]
224 # Get ready for the next task
227 if task == 'download_version_check':
228 print(_('Downloading'), self.common.paths['version_check_url'])
229 self.download('version check', self.common.paths['version_check_url'], self.common.paths['version_check_file'])
231 if task == 'set_version':
232 version = self.get_stable_version()
234 self.common.build_paths(self.get_stable_version())
235 print(_('Latest version: {}').format(version))
238 self.set_state('error', _("Error detecting Tor Browser version."), [], False)
241 elif task == 'download_sig':
242 print(_('Downloading'), self.common.paths['sig_url'].format(self.common.settings['mirror']))
243 self.download('signature', self.common.paths['sig_url'], self.common.paths['sig_file'])
245 elif task == 'download_tarball':
246 print(_('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror']))
247 if not self.force_redownload and os.path.exists(self.common.paths['tarball_file']):
250 self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
252 elif task == 'verify':
253 print(_('Verifying Signature'))
256 elif task == 'extract':
257 print(_('Extracting'), self.common.paths['tarball_filename'])
261 print(_('Running'), self.common.paths['tbb']['start'])
264 elif task == 'start_over':
265 print(_('Starting download over again'))
268 def download(self, name, url, path):
269 # Download from the selected mirror
270 mirror_url = url.format(self.common.settings['mirror']).encode()
272 # Initialize the progress bar
273 self.progress_bar.setValue(0)
274 self.progress_bar.setMaximum(100)
275 if self.common.settings['download_over_tor']:
276 self.progress_bar.setFormat(_('Downloading') + ' {0} '.format(name) + _('(over Tor)') + ', %p%')
278 self.progress_bar.setFormat(_('Downloading') + ' {0}, %p%'.format(name))
280 def progress_update(total_bytes, bytes_so_far):
281 percent = float(bytes_so_far) / float(total_bytes)
282 amount = float(bytes_so_far)
284 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
287 amount /= float(size)
290 message = _('Downloaded') + (' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units))
291 if self.common.settings['download_over_tor']:
292 message += ' ' + _('(over Tor)')
294 self.progress_bar.setMaximum(total_bytes)
295 self.progress_bar.setValue(bytes_so_far)
296 self.progress_bar.setFormat(message)
298 def download_complete():
299 # Download complete, next task
302 def download_error(gui, message):
304 self.set_state(gui, message, [], False)
307 t = DownloadThread(self.common, mirror_url, path)
308 t.progress_update.connect(progress_update)
309 t.download_complete.connect(download_complete)
310 t.download_error.connect(download_error)
314 def try_default_mirror(self):
315 # change mirror to default and relaunch TBL
316 self.common.settings['mirror'] = self.common.default_mirror
317 self.common.save_settings()
318 subprocess.Popen([self.common.paths['tbl_bin']])
321 def try_forcing_english(self):
322 # change force english to true and relaunch TBL
323 self.common.settings['force_en-US'] = True
324 self.common.save_settings()
325 subprocess.Popen([self.common.paths['tbl_bin']])
329 # set download_over_tor to true and relaunch TBL
330 self.common.settings['download_over_tor'] = True
331 self.common.save_settings()
332 subprocess.Popen([self.common.paths['tbl_bin']])
335 def get_stable_version(self):
336 tree = ET.parse(self.common.paths['version_check_file'])
337 for up in tree.getroot():
338 if up.tag == 'update' and up.attrib['appVersion']:
339 version = str(up.attrib['appVersion'])
341 # make sure the version does not contain directory traversal attempts
342 # e.g. "5.5.3", "6.0a", "6.0a-hardened" are valid but "../../../../.." is invalid
343 if not re.match(r'^[a-z0-9\.\-]+$', version):
350 self.progress_bar.setValue(0)
351 self.progress_bar.setMaximum(0)
352 self.progress_bar.show()
354 self.label.setText(_('Verifying Signature'))
360 sigerror = 'SIGNATURE VERIFICATION FAILED!\n\nError Code: {0}\n\nYou might be under attack, there might' \
361 ' be a network\nproblem, or you may be missing a recently added\nTor Browser verification key.' \
362 '\nClick Start to refresh the keyring and try again. If the message persists report the above' \
363 ' error code here:\nhttps://github.com/micahflee/torbrowser-launcher/issues'.format(message)
365 self.set_state('task', sigerror, ['start_over'], False)
368 t = VerifyThread(self.common)
369 t.error.connect(error)
370 t.success.connect(success)
375 self.progress_bar.setValue(0)
376 self.progress_bar.setMaximum(0)
377 self.progress_bar.show()
379 self.label.setText(_('Installing'))
387 _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])),
388 ['start_over'], False
392 t = ExtractThread(self.common)
393 t.error.connect(error)
394 t.success.connect(success)
398 def check_min_version(self):
399 installed_version = None
400 for line in open(self.common.paths['tbb']['changelog']).readlines():
401 if line.startswith('Tor Browser '):
402 installed_version = line.split()[2]
405 if self.min_version <= installed_version:
410 def run(self, run_next_task=True):
411 # Don't run if it isn't at least the minimum version
412 if not self.check_min_version():
413 message = _("The version of Tor Browser you have installed is earlier than it should be, which could be a "
414 "sign of an attack!")
417 Alert(self.common, message)
420 # Hide the TBL window (#151)
424 subprocess.call([self.common.paths['tbb']['start']], cwd=self.common.paths['tbb']['dir_tbb'])
429 # Start over and download TBB again
430 def start_over(self):
431 self.force_redownload = True # Overwrite any existing file
432 self.label.setText(_("Downloading Tor Browser Bundle over again."))
433 self.gui_tasks = ['download_tarball', 'verify', 'extract', 'run']
437 def closeEvent(self, event):
438 # Clear the download cache
440 os.remove(self.common.paths['version_check_file'])
441 os.remove(self.common.paths['sig_file'])
442 os.remove(self.common.paths['tarball_file'])
446 super(Launcher, self).closeEvent(event)
449 class Alert(QtWidgets.QMessageBox):
453 def __init__(self, common, message, icon=QtWidgets.QMessageBox.NoIcon, buttons=QtWidgets.QMessageBox.Ok, autostart=True):
454 super(Alert, self).__init__(None)
456 self.setWindowTitle(_("Tor Browser Launcher"))
457 self.setWindowIcon(QtGui.QIcon(common.paths['icon_file']))
458 self.setText(message)
460 self.setStandardButtons(buttons)
466 class DownloadThread(QtCore.QThread):
468 Download a file in a separate thread.
470 progress_update = QtCore.pyqtSignal(int, int)
471 download_complete = QtCore.pyqtSignal()
472 download_error = QtCore.pyqtSignal(str, str)
474 def __init__(self, common, url, path):
475 super(DownloadThread, self).__init__()
480 # Use tor socks5 proxy, if enabled
481 if self.common.settings['download_over_tor']:
482 socks5_address = 'socks5://{}'.format(self.common.settings['tor_socks_address'])
484 'https': socks5_address,
485 'http': socks5_address
491 with open(self.path, "wb") as f:
494 r = requests.get(self.url,
495 headers={'User-Agent': 'torbrowser-launcher'},
496 stream=True, proxies=self.proxies)
498 # If status code isn't 200, something went wrong
499 if r.status_code != 200:
500 # Should we use the default mirror?
501 if self.common.settings['mirror'] != self.common.default_mirror:
502 message = (_("Download Error:") +
503 " {0}\n\n" + _("You are currently using a non-default mirror") +
504 ":\n{1}\n\n" + _("Would you like to switch back to the default?")).format(
505 r.status_code, self.common.settings['mirror']
507 self.download_error.emit('error_try_default_mirror', message)
509 # Should we switch to English?
510 elif self.common.language != 'en-US' and not self.common.settings['force_en-US']:
511 message = (_("Download Error:") +
513 _("Would you like to try the English version of Tor Browser instead?")).format(
516 self.download_error.emit('error_try_forcing_english', message)
519 message = (_("Download Error:") + " {0}").format(r.status_code)
520 self.download_error.emit('error', message)
525 # Start streaming the download
526 total_bytes = int(r.headers.get('content-length'))
528 for data in r.iter_content(chunk_size=4096):
529 bytes_so_far += len(data)
531 self.progress_update.emit(total_bytes, bytes_so_far)
533 except requests.exceptions.SSLError:
534 message = _('Invalid SSL certificate for:\n{0}\n\nYou may be under attack.').format(self.url.decode())
535 if not self.common.settings['download_over_tor']:
536 message += "\n\n" + _('Try the download again using Tor?')
537 self.download_error.emit('error_try_tor', message)
539 self.download_error.emit('error', message)
542 except requests.exceptions.ConnectionError:
544 if self.common.settings['download_over_tor']:
545 message = _("Error starting download:\n\n{0}\n\nTrying to download over Tor. "
546 "Are you sure Tor is configured correctly and running?").format(self.url.decode())
547 self.download_error.emit('error', message)
549 message = _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(
552 self.download_error.emit('error', message)
556 self.download_complete.emit()
559 class VerifyThread(QtCore.QThread):
561 Verify the signature in a separate thread
563 success = QtCore.pyqtSignal()
564 error = QtCore.pyqtSignal(str)
566 def __init__(self, common):
567 super(VerifyThread, self).__init__()
571 with gpg.Context() as c:
572 c.set_engine_info(gpg.constants.protocol.OpenPGP, home_dir=self.common.paths['gnupg_homedir'])
574 sig = gpg.Data(file=self.common.paths['sig_file'])
575 signed = gpg.Data(file=self.common.paths['tarball_file'])
578 c.verify(signature=sig, signed_data=signed)
579 except gpg.errors.BadSignatures as e:
580 result = str(e).split(": ")
581 if result[1] == 'No public key':
582 self.common.refresh_keyring(result[0])
583 self.error.emit(str(e))
588 class ExtractThread(QtCore.QThread):
590 Extract the tarball in a separate thread
592 success = QtCore.pyqtSignal()
593 error = QtCore.pyqtSignal()
595 def __init__(self, common):
596 super(ExtractThread, self).__init__()
602 if self.common.paths['tarball_file'][-2:] == 'xz':
603 # if tarball is .tar.xz
604 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
605 tf = tarfile.open(fileobj=xz)
606 tf.extractall(self.common.paths['tbb']['dir'])
609 # if tarball is .tar.gz
610 if tarfile.is_tarfile(self.common.paths['tarball_file']):
611 tf = tarfile.open(self.common.paths['tarball_file'])
612 tf.extractall(self.common.paths['tbb']['dir'])