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.
42 import xml.etree.ElementTree as ET
44 from PyQt5 import QtCore, QtWidgets, QtGui
47 class TryStableException(Exception):
51 class TryDefaultMirrorException(Exception):
55 class TryForcingEnglishException(Exception):
59 class DownloadErrorException(Exception):
63 class Launcher(QtWidgets.QMainWindow):
67 def __init__(self, common, app, url_list):
68 super(Launcher, self).__init__()
72 self.url_list = url_list
73 self.force_redownload = False
75 # This is the current version of Tor Browser, which should get updated with every release
76 self.min_version = '7.5.2'
79 self.set_state(None, '', [])
80 self.launch_gui = True
82 # If Tor Browser is not installed, detect latest version, download, and install
83 if not self.common.settings['installed'] or not self.check_min_version():
84 # Different message if downloading for the first time, or because your installed version is too low
86 if not self.common.settings['installed']:
87 download_message = _("Downloading and installing Tor Browser for the first time.")
88 elif not self.check_min_version():
89 download_message = _("Your version of Tor Browser is out-of-date. Downloading and installing the newest version.")
91 # Download and install
92 print(download_message)
93 self.set_state('task', download_message,
94 ['download_version_check',
102 if self.common.settings['download_over_tor']:
103 print(_('Downloading over Tor'))
106 # Tor Browser is already installed, so run
108 self.launch_gui = False
111 # Build the rest of the UI
114 self.setWindowTitle(_("Tor Browser"))
115 self.setWindowIcon(QtGui.QIcon(self.common.paths['icon_file']))
118 self.label = QtWidgets.QLabel()
121 self.progress_bar = QtWidgets.QProgressBar()
122 self.progress_bar.setTextVisible(True)
123 self.progress_bar.setMinimum(0)
124 self.progress_bar.setMaximum(0)
125 self.progress_bar.setValue(0)
128 self.yes_button = QtWidgets.QPushButton()
129 self.yes_button.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_DialogApplyButton))
130 self.yes_button.clicked.connect(self.yes_clicked)
131 self.start_button = QtWidgets.QPushButton()
132 self.start_button.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_DialogApplyButton))
133 self.start_button.clicked.connect(self.start)
134 self.cancel_button = QtWidgets.QPushButton()
135 self.cancel_button.setIcon(self.style().standardIcon(QtWidgets.QStyle.SP_DialogCancelButton))
136 self.cancel_button.clicked.connect(self.close)
137 buttons_layout = QtWidgets.QHBoxLayout()
138 buttons_layout.addStretch()
139 buttons_layout.addWidget(self.yes_button)
140 buttons_layout.addWidget(self.start_button)
141 buttons_layout.addWidget(self.cancel_button)
142 buttons_layout.addStretch()
145 layout = QtWidgets.QVBoxLayout()
146 layout.addWidget(self.label)
147 layout.addWidget(self.progress_bar)
148 layout.addLayout(buttons_layout)
150 central_widget = QtWidgets.QWidget()
151 central_widget.setLayout(layout)
152 self.setCentralWidget(central_widget)
157 # Set the current state of Tor Browser Launcher
158 def set_state(self, gui, message, tasks, autostart=True):
160 self.gui_message = message
161 self.gui_tasks = tasks
163 self.gui_autostart = autostart
165 # Show and hide parts of the UI based on the current state
168 self.progress_bar.hide()
169 self.yes_button.hide()
170 self.start_button.hide()
172 if 'error' in self.gui:
174 self.label.setText(self.gui_message)
177 if self.gui != 'error':
178 self.yes_button.setText(_('Yes'))
179 self.yes_button.show()
182 self.cancel_button.setText(_('Exit'))
184 elif self.gui == 'task':
186 self.label.setText(self.gui_message)
189 self.progress_bar.show()
192 if not self.gui_autostart:
193 self.start_button.show()
196 self.cancel_button.setText(_('Cancel'))
198 if self.gui_autostart:
201 # Yes button clicked, based on the state decide what to do
202 def yes_clicked(self):
203 if self.gui == 'error_try_stable':
205 elif self.gui == 'error_try_default_mirror':
206 self.try_default_mirror()
207 elif self.gui == 'error_try_forcing_english':
208 self.try_forcing_english()
209 elif self.gui == 'error_try_tor':
212 # Start button clicked, begin tasks
213 def start(self, widget, data=None):
214 # Hide the start button
215 self.start_button.hide()
217 # Start running tasks
220 # Run the next task in the task list
222 if self.gui_task_i >= len(self.gui_tasks):
226 task = self.gui_tasks[self.gui_task_i]
228 # Get ready for the next task
231 if task == 'download_version_check':
232 print(_('Downloading'), self.common.paths['version_check_url'])
233 self.download('version check', self.common.paths['version_check_url'], self.common.paths['version_check_file'])
235 if task == 'set_version':
236 version = self.get_stable_version()
238 self.common.build_paths(self.get_stable_version())
239 print(_('Latest version: {}').format(version))
242 self.set_state('error', _("Error detecting Tor Browser version."), [], False)
245 elif task == 'download_sig':
246 print(_('Downloading'), self.common.paths['sig_url'].format(self.common.settings['mirror']))
247 self.download('signature', self.common.paths['sig_url'], self.common.paths['sig_file'])
249 elif task == 'download_tarball':
250 print(_('Downloading'), self.common.paths['tarball_url'].format(self.common.settings['mirror']))
251 if not self.force_redownload and os.path.exists(self.common.paths['tarball_file']):
254 self.download('tarball', self.common.paths['tarball_url'], self.common.paths['tarball_file'])
256 elif task == 'verify':
257 print(_('Verifying Signature'))
260 elif task == 'extract':
261 print(_('Extracting'), self.common.paths['tarball_filename'])
265 print(_('Running'), self.common.paths['tbb']['start'])
268 elif task == 'start_over':
269 print(_('Starting download over again'))
272 def download(self, name, url, path):
273 # Download from the selected mirror
274 mirror_url = url.format(self.common.settings['mirror']).encode()
276 # Initialize the progress bar
277 self.progress_bar.setValue(0)
278 self.progress_bar.setMaximum(100)
279 if self.common.settings['download_over_tor']:
280 self.progress_bar.setFormat(_('Downloading') + ' {0} '.format(name) + _('(over Tor)') + ', %p%')
282 self.progress_bar.setFormat(_('Downloading') + ' {0}, %p%'.format(name))
284 def progress_update(total_bytes, bytes_so_far):
285 percent = float(bytes_so_far) / float(total_bytes)
286 amount = float(bytes_so_far)
288 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
291 amount /= float(size)
294 message = _('Downloaded') + (' %2.1f%% (%2.1f %s)' % ((percent * 100.0), amount, units))
295 if self.common.settings['download_over_tor']:
296 message += ' ' + _('(over Tor)')
298 self.progress_bar.setMaximum(total_bytes)
299 self.progress_bar.setValue(bytes_so_far)
300 self.progress_bar.setFormat(message)
302 def download_complete():
303 # Download complete, next task
306 def download_error(gui, message):
308 self.set_state(gui, message, [], False)
311 t = DownloadThread(self.common, mirror_url, path)
312 t.progress_update.connect(progress_update)
313 t.download_complete.connect(download_complete)
314 t.download_error.connect(download_error)
318 def try_default_mirror(self):
319 # change mirror to default and relaunch TBL
320 self.common.settings['mirror'] = self.common.default_mirror
321 self.common.save_settings()
322 subprocess.Popen([self.common.paths['tbl_bin']])
325 def try_forcing_english(self):
326 # change force english to true and relaunch TBL
327 self.common.settings['force_en-US'] = True
328 self.common.save_settings()
329 subprocess.Popen([self.common.paths['tbl_bin']])
333 # set download_over_tor to true and relaunch TBL
334 self.common.settings['download_over_tor'] = True
335 self.common.save_settings()
336 subprocess.Popen([self.common.paths['tbl_bin']])
339 def get_stable_version(self):
340 tree = ET.parse(self.common.paths['version_check_file'])
341 for up in tree.getroot():
342 if up.tag == 'update' and up.attrib['appVersion']:
343 version = str(up.attrib['appVersion'])
345 # make sure the version does not contain directory traversal attempts
346 # e.g. "5.5.3", "6.0a", "6.0a-hardened" are valid but "../../../../.." is invalid
347 if not re.match(r'^[a-z0-9\.\-]+$', version):
354 self.progress_bar.setValue(0)
355 self.progress_bar.setMaximum(0)
356 self.progress_bar.show()
358 self.label.setText(_('Verifying Signature'))
364 sigerror = 'SIGNATURE VERIFICATION FAILED!\n\nError Code: {0}\n\nYou might be under attack, there might' \
365 ' be a network\nproblem, or you may be missing a recently added\nTor Browser verification key.' \
366 '\nClick Start to refresh the keyring and try again. If the message persists report the above' \
367 ' error code here:\nhttps://github.com/micahflee/torbrowser-launcher/issues'.format(sigerror)
369 self.set_state('task', sigerror, ['start_over'], False)
372 t = VerifyThread(self.common)
373 t.error.connect(error)
374 t.success.connect(success)
379 self.progress_bar.setValue(0)
380 self.progress_bar.setMaximum(0)
381 self.progress_bar.show()
383 self.label.setText(_('Installing'))
389 self.set_state('task', _("Tor Browser Launcher doesn't understand the file format of {0}".format(self.common.paths['tarball_file'])), ['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(r.status_code, self.common.settings['mirror'])
505 self.download_error.emit('error_try_default_mirror', message)
507 # Should we switch to English?
508 elif self.common.language != 'en-US' and not self.common.settings['force_en-US']:
509 message = (_("Download Error:") +
510 " {0}\n\n" + _("Would you like to try the English version of Tor Browser instead?")).format(r.status_code)
511 self.download_error.emit('error_try_forcing_english', message)
514 message = (_("Download Error:") + " {0}").format(r.status_code)
515 self.download_error.emit('error', message)
520 # Start streaming the download
521 total_bytes = int(r.headers.get('content-length'))
523 for data in r.iter_content(chunk_size=4096):
524 bytes_so_far += len(data)
526 self.progress_update.emit(total_bytes, bytes_so_far)
528 except requests.exceptions.SSLError:
529 if not self.common.settings['download_over_tor']:
530 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?')
531 self.download_error.emit('error_try_tor', message)
533 message = _('Invalid SSL certificate for:\n{0}\n\nYou may be under attack.'.format(self.url.decode()))
534 self.download_error.emit('error', message)
537 except requests.exceptions.ConnectionError:
539 if self.common.settings['download_over_tor']:
540 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())
541 self.download_error.emit('error', message)
543 message = _("Error starting download:\n\n{0}\n\nAre you connected to the internet?").format(self.url.decode())
544 self.download_error.emit('error', message)
548 self.download_complete.emit()
551 class VerifyThread(QtCore.QThread):
553 Verify the signature in a separate thread
555 success = QtCore.pyqtSignal()
556 error = QtCore.pyqtSignal(str)
558 def __init__(self, common):
559 super(VerifyThread, self).__init__()
563 with gpg.Context() as c:
564 c.set_engine_info(gpg.constants.protocol.OpenPGP, home_dir=self.common.paths['gnupg_homedir'])
566 sig = gpg.Data(file=self.common.paths['sig_file'])
567 signed = gpg.Data(file=self.common.paths['tarball_file'])
570 c.verify(signature=sig, signed_data=signed)
571 except gpg.errors.BadSignatures as e:
572 result = str(e).split(": ")
573 if result[1] == 'No public key':
574 self.common.refresh_keyring(result[0])
575 self.error.emit(str(e))
580 class ExtractThread(QtCore.QThread):
582 Extract the tarball in a separate thread
584 success = QtCore.pyqtSignal()
585 error = QtCore.pyqtSignal()
587 def __init__(self, common):
588 super(ExtractThread, self).__init__()
594 if self.common.paths['tarball_file'][-2:] == 'xz':
595 # if tarball is .tar.xz
596 xz = lzma.LZMAFile(self.common.paths['tarball_file'])
597 tf = tarfile.open(fileobj=xz)
598 tf.extractall(self.common.paths['tbb']['dir'])
601 # if tarball is .tar.gz
602 if tarfile.is_tarfile(self.common.paths['tarball_file']):
603 tf = tarfile.open(self.common.paths['tarball_file'])
604 tf.extractall(self.common.paths['tbb']['dir'])