3 https://github.com/micahflee/torbrowser-launcher/
5 Copyright (c) 2013-2021 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.
39 import xml.etree.ElementTree as ET
40 from packaging import version
42 from PyQt5 import QtCore, QtWidgets, QtGui
45 class TryStableException(Exception):
49 class TryDefaultMirrorException(Exception):
53 class TryForcingEnglishException(Exception):
57 class DownloadErrorException(Exception):
61 class Launcher(QtWidgets.QMainWindow):
66 def __init__(self, common, app, url_list):
67 super(Launcher, self).__init__()
71 self.url_list = url_list
72 self.force_redownload = False
74 # This is the current version of Tor Browser, which should get updated with every release
75 self.min_version = "7.5.2"
78 self.set_state(None, "", [])
79 self.launch_gui = True
81 # If Tor Browser is not installed, detect latest version, download, and install
82 if not self.common.settings["installed"] or not self.check_min_version():
83 # Different message if downloading for the first time, or because your installed version is too low
85 if not self.common.settings["installed"]:
86 download_message = _("Downloading Tor Browser for the first time.")
87 elif not self.check_min_version():
89 "Your version of Tor Browser is out-of-date. "
90 "Downloading the newest version."
93 # Download and install
94 print(download_message)
99 "download_version_check",
109 if self.common.settings["download_over_tor"]:
110 print(_("Downloading over Tor"))
113 # Tor Browser is already installed, so run
114 launch_message = "Launching Tor Browser."
115 print(launch_message)
116 self.set_state("task", launch_message, ["run"])
118 # Build the rest of the UI
121 self.setWindowTitle(_("Tor Browser"))
122 self.setWindowIcon(QtGui.QIcon(self.common.paths["icon_file"]))
125 self.label = QtWidgets.QLabel()
128 self.progress_bar = QtWidgets.QProgressBar()
129 self.progress_bar.setTextVisible(True)
130 self.progress_bar.setMinimum(0)
131 self.progress_bar.setMaximum(0)
132 self.progress_bar.setValue(0)
135 self.yes_button = QtWidgets.QPushButton()
136 self.yes_button.setIcon(
137 self.style().standardIcon(QtWidgets.QStyle.SP_DialogApplyButton)
139 self.yes_button.clicked.connect(self.yes_clicked)
140 self.start_button = QtWidgets.QPushButton(_("Start"))
141 self.start_button.setIcon(
142 self.style().standardIcon(QtWidgets.QStyle.SP_DialogApplyButton)
144 self.start_button.clicked.connect(self.start)
145 self.cancel_button = QtWidgets.QPushButton()
146 self.cancel_button.setIcon(
147 self.style().standardIcon(QtWidgets.QStyle.SP_DialogCancelButton)
149 self.cancel_button.clicked.connect(self.close)
150 buttons_layout = QtWidgets.QHBoxLayout()
151 buttons_layout.addStretch()
152 buttons_layout.addWidget(self.yes_button)
153 buttons_layout.addWidget(self.start_button)
154 buttons_layout.addWidget(self.cancel_button)
155 buttons_layout.addStretch()
158 layout = QtWidgets.QVBoxLayout()
159 layout.addWidget(self.label)
160 layout.addWidget(self.progress_bar)
161 layout.addLayout(buttons_layout)
163 central_widget = QtWidgets.QWidget()
164 central_widget.setLayout(layout)
165 self.setCentralWidget(central_widget)
169 # Set the current state of Tor Browser Launcher
170 def set_state(self, gui, message, tasks, autostart=True):
172 self.gui_message = message
173 self.gui_tasks = tasks
175 self.gui_autostart = autostart
177 # Show and hide parts of the UI based on the current state
180 self.progress_bar.hide()
181 self.yes_button.hide()
182 self.start_button.hide()
184 if "error" in self.gui:
186 self.label.setText(self.gui_message)
189 if self.gui != "error":
190 self.yes_button.setText(_("Yes"))
191 self.yes_button.show()
194 self.cancel_button.setText(_("Exit"))
196 elif self.gui == "task":
198 self.label.setText(self.gui_message)
201 self.progress_bar.show()
204 if not self.gui_autostart:
205 self.start_button.show()
208 self.cancel_button.setText(_("Cancel"))
213 if self.gui_autostart:
216 # Yes button clicked, based on the state decide what to do
217 def yes_clicked(self):
218 if self.gui == "error_try_stable":
220 elif self.gui == "error_try_default_mirror":
221 self.try_default_mirror()
222 elif self.gui == "error_try_forcing_english":
223 self.try_forcing_english()
224 elif self.gui == "error_try_tor":
227 # Start button clicked, begin tasks
228 def start(self, widget, data=None):
229 # Hide the start button
230 self.start_button.hide()
232 # Start running tasks
235 # Run the next task in the task list
237 if self.gui_task_i >= len(self.gui_tasks):
241 task = self.gui_tasks[self.gui_task_i]
243 # Get ready for the next task
246 if task == "download_version_check":
247 print(_("Downloading"), self.common.paths["version_check_url"])
250 self.common.paths["version_check_url"],
251 self.common.paths["version_check_file"],
254 if task == "set_version":
255 version = self.get_stable_version()
257 self.common.build_paths(self.get_stable_version())
258 print(_("Latest version: {}").format(version))
262 "error", _("Error detecting Tor Browser version."), [], False
266 elif task == "download_sig":
269 self.common.paths["sig_url"].format(self.common.settings["mirror"]),
272 "signature", self.common.paths["sig_url"], self.common.paths["sig_file"]
275 elif task == "download_tarball":
278 self.common.paths["tarball_url"].format(self.common.settings["mirror"]),
280 if not self.force_redownload and os.path.exists(
281 self.common.paths["tarball_file"]
287 self.common.paths["tarball_url"],
288 self.common.paths["tarball_file"],
291 elif task == "verify":
292 print(_("Verifying Signature"))
295 elif task == "extract":
296 print(_("Extracting"), self.common.paths["tarball_filename"])
300 print(_("Running"), self.common.paths["tbb"]["start"])
303 elif task == "start_over":
304 print(_("Starting download over again"))
307 def download(self, name, url, path):
308 # Download from the selected mirror
309 mirror_url = url.format(self.common.settings["mirror"]).encode()
311 # Initialize the progress bar
312 self.progress_bar.setValue(0)
313 self.progress_bar.setMaximum(100)
314 if self.common.settings["download_over_tor"]:
315 self.progress_bar.setFormat(
316 _("Downloading") + " {0} ".format(name) + _("(over Tor)") + ", %p%"
319 self.progress_bar.setFormat(_("Downloading") + " {0}, %p%".format(name))
321 def progress_update(total_bytes, bytes_so_far):
322 percent = float(bytes_so_far) / float(total_bytes)
323 amount = float(bytes_so_far)
325 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
328 amount /= float(size)
331 message = _("Downloaded") + (
332 " %2.1f%% (%2.1f %s)" % ((percent * 100.0), amount, units)
334 if self.common.settings["download_over_tor"]:
335 message += " " + _("(over Tor)")
337 self.progress_bar.setMaximum(total_bytes)
338 self.progress_bar.setValue(bytes_so_far)
339 self.progress_bar.setFormat(message)
341 def download_complete():
342 # Download complete, next task
345 def download_error(gui, message):
347 self.set_state(gui, message, [], False)
350 t = DownloadThread(self.common, mirror_url, path)
351 t.progress_update.connect(progress_update)
352 t.download_complete.connect(download_complete)
353 t.download_error.connect(download_error)
357 def try_default_mirror(self):
358 # change mirror to default and relaunch TBL
359 self.common.settings["mirror"] = self.common.default_mirror
360 self.common.save_settings()
361 subprocess.Popen([self.common.paths["tbl_bin"]])
364 def try_forcing_english(self):
365 # change force english to true and relaunch TBL
366 self.common.settings["force_en-US"] = True
367 self.common.save_settings()
368 subprocess.Popen([self.common.paths["tbl_bin"]])
372 # set download_over_tor to true and relaunch TBL
373 self.common.settings["download_over_tor"] = True
374 self.common.save_settings()
375 subprocess.Popen([self.common.paths["tbl_bin"]])
378 def get_stable_version(self):
379 tree = ET.parse(self.common.paths["version_check_file"])
380 for up in tree.getroot():
381 if up.tag == "update" and up.attrib["appVersion"]:
382 version = str(up.attrib["appVersion"])
384 # make sure the version does not contain directory traversal attempts
385 # e.g. "5.5.3", "6.0a", "6.0a-hardened" are valid but "../../../../.." is invalid
386 if not re.match(r"^[a-z0-9\.\-]+$", version):
393 self.progress_bar.setValue(0)
394 self.progress_bar.setMaximum(0)
395 self.progress_bar.show()
397 self.label.setText(_("Verifying Signature"))
403 # Make backup of tarball and sig
404 backup_tarball_filename = (
405 self.common.paths["tarball_file"] + ".verification_failed"
407 backup_sig_filename = self.common.paths["sig_file"] + ".verification_failed"
408 shutil.copyfile(self.common.paths["tarball_file"], backup_tarball_filename)
409 shutil.copyfile(self.common.paths["sig_file"], backup_sig_filename)
412 "SIGNATURE VERIFICATION FAILED!\n\n"
413 "Error Code: {0}\n\n"
414 "You might be under attack, there might be a network problem, or you may be missing a "
415 "recently added Tor Browser verification key.\n\n"
416 "A copy of the Tor Browser files you downloaded have been saved here:\n"
418 "Click Start to refresh the keyring and try again. If the message persists report the above "
419 "error code here:\nhttps://github.com/micahflee/torbrowser-launcher/issues"
421 sigerror = sigerror.format(
422 message, backup_tarball_filename, backup_sig_filename
425 self.set_state("task", sigerror, ["start_over"], False)
428 t = VerifyThread(self.common)
429 t.error.connect(error)
430 t.success.connect(success)
435 self.progress_bar.setValue(0)
436 self.progress_bar.setMaximum(0)
437 self.progress_bar.show()
439 self.label.setText(_("Installing"))
448 "Tor Browser Launcher doesn't understand the file format of {0}".format(
449 self.common.paths["tarball_file"]
457 t = ExtractThread(self.common)
458 t.error.connect(error)
459 t.success.connect(success)
463 def check_min_version(self):
464 installed_version = None
465 for line in open(self.common.paths["tbb"]["changelog"], "rb").readlines():
466 if line.startswith(b"Tor Browser "):
467 installed_version = line.split()[2].decode()
470 if version.parse(self.min_version) <= version.parse(installed_version):
476 # Don't run if it isn't at least the minimum version
477 if not self.check_min_version():
479 "The version of Tor Browser you have installed is earlier than it should be, which could be a "
484 Alert(self.common, message)
489 [self.common.paths["tbb"]["start"]], cwd=self.common.paths["tbb"]["dir_tbb"]
493 # Start over and download TBB again
494 def start_over(self):
495 self.force_redownload = True # Overwrite any existing file
496 self.label.setText(_("Downloading Tor Browser over again."))
497 self.gui_tasks = ["download_tarball", "verify", "extract", "run"]
501 def closeEvent(self, event):
502 # Clear the download cache
504 os.remove(self.common.paths["version_check_file"])
505 os.remove(self.common.paths["sig_file"])
506 os.remove(self.common.paths["tarball_file"])
510 super(Launcher, self).closeEvent(event)
513 class Alert(QtWidgets.QMessageBox):
522 icon=QtWidgets.QMessageBox.NoIcon,
523 buttons=QtWidgets.QMessageBox.Ok,
526 super(Alert, self).__init__(None)
528 self.setWindowTitle(_("Tor Browser Launcher"))
529 self.setWindowIcon(QtGui.QIcon(common.paths["icon_file"]))
530 self.setText(message)
532 self.setStandardButtons(buttons)
538 class DownloadThread(QtCore.QThread):
540 Download a file in a separate thread.
543 progress_update = QtCore.pyqtSignal(int, int)
544 download_complete = QtCore.pyqtSignal()
545 download_error = QtCore.pyqtSignal(str, str)
547 def __init__(self, common, url, path):
548 super(DownloadThread, self).__init__()
553 # Use tor socks5 proxy, if enabled
554 if self.common.settings['download_over_tor']:
555 socks5_address = 'socks5h://{}'.format(self.common.settings['tor_socks_address'])
557 'https': socks5_address,
558 'http': socks5_address
564 with open(self.path, "wb") as f:
569 headers={"User-Agent": "torbrowser-launcher"},
571 proxies=self.proxies,
574 # If status code isn't 200, something went wrong
575 if r.status_code != 200:
576 # Should we use the default mirror?
577 if self.common.settings["mirror"] != self.common.default_mirror:
581 + _("You are currently using a non-default mirror")
583 + _("Would you like to switch back to the default?")
584 ).format(r.status_code, self.common.settings["mirror"])
585 self.download_error.emit("error_try_default_mirror", message)
587 # Should we switch to English?
589 self.common.language != "en-US"
590 and not self.common.settings["force_en-US"]
596 "Would you like to try the English version of Tor Browser instead?"
598 ).format(r.status_code)
599 self.download_error.emit("error_try_forcing_english", message)
602 message = (_("Download Error:") + " {0}").format(r.status_code)
603 self.download_error.emit("error", message)
608 # Start streaming the download
609 total_bytes = int(r.headers.get("content-length"))
611 for data in r.iter_content(chunk_size=4096):
612 bytes_so_far += len(data)
614 self.progress_update.emit(total_bytes, bytes_so_far)
616 except requests.exceptions.SSLError:
618 "Invalid SSL certificate for:\n{0}\n\nYou may be under attack."
619 ).format(self.url.decode())
620 if not self.common.settings["download_over_tor"]:
621 message += "\n\n" + _("Try the download again using Tor?")
622 self.download_error.emit("error_try_tor", message)
624 self.download_error.emit("error", message)
627 except requests.exceptions.ConnectionError:
629 if self.common.settings["download_over_tor"]:
631 "Error starting download:\n\n{0}\n\nTrying to download over Tor. "
632 "Are you sure Tor is configured correctly and running?"
633 ).format(self.url.decode())
634 self.download_error.emit("error", message)
637 "Error starting download:\n\n{0}\n\nAre you connected to the internet?"
638 ).format(self.url.decode())
639 self.download_error.emit("error", message)
643 self.download_complete.emit()
646 class VerifyThread(QtCore.QThread):
648 Verify the signature in a separate thread
651 success = QtCore.pyqtSignal()
652 error = QtCore.pyqtSignal(str)
654 def __init__(self, common):
655 super(VerifyThread, self).__init__()
659 def verify(second_try=False):
660 with gpg.Context() as c:
662 gpg.constants.protocol.OpenPGP,
663 home_dir=self.common.paths["gnupg_homedir"],
666 sig = gpg.Data(file=self.common.paths["sig_file"])
667 signed = gpg.Data(file=self.common.paths["tarball_file"])
670 c.verify(signature=sig, signed_data=signed)
671 except gpg.errors.BadSignatures as e:
673 self.error.emit(str(e))
683 # If it fails, refresh the keyring and try again
684 self.common.refresh_keyring()
688 class ExtractThread(QtCore.QThread):
690 Extract the tarball in a separate thread
693 success = QtCore.pyqtSignal()
694 error = QtCore.pyqtSignal()
696 def __init__(self, common):
697 super(ExtractThread, self).__init__()
703 if self.common.paths["tarball_file"][-2:] == "xz":
704 # if tarball is .tar.xz
705 xz = lzma.LZMAFile(self.common.paths["tarball_file"])
706 tf = tarfile.open(fileobj=xz)
707 tf.extractall(self.common.paths["tbb"]["dir"])
710 # if tarball is .tar.gz
711 if tarfile.is_tarfile(self.common.paths["tarball_file"]):
712 tf = tarfile.open(self.common.paths["tarball_file"])
713 tf.extractall(self.common.paths["tbb"]["dir"])