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__()
69 self.common.refresh_keyring()
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 Tor Browser for the first time.")
88 elif not self.check_min_version():
90 "Your version of Tor Browser is out-of-date. "
91 "Downloading the newest version."
94 # Download and install
95 print(download_message)
100 "download_version_check",
110 if self.common.settings["download_over_tor"]:
111 print(_("Downloading over Tor"))
114 # Tor Browser is already installed, so run
115 launch_message = "Launching Tor Browser."
116 print(launch_message)
117 self.set_state("task", launch_message, ["run"])
119 # Build the rest of the UI
122 self.setWindowTitle(_("Tor Browser"))
123 self.setWindowIcon(QtGui.QIcon(self.common.paths["icon_file"]))
126 self.label = QtWidgets.QLabel()
129 self.progress_bar = QtWidgets.QProgressBar()
130 self.progress_bar.setTextVisible(True)
131 self.progress_bar.setMinimum(0)
132 self.progress_bar.setMaximum(0)
133 self.progress_bar.setValue(0)
136 self.yes_button = QtWidgets.QPushButton()
137 self.yes_button.setIcon(
138 self.style().standardIcon(QtWidgets.QStyle.SP_DialogApplyButton)
140 self.yes_button.clicked.connect(self.yes_clicked)
141 self.start_button = QtWidgets.QPushButton(_("Start"))
142 self.start_button.setIcon(
143 self.style().standardIcon(QtWidgets.QStyle.SP_DialogApplyButton)
145 self.start_button.clicked.connect(self.start)
146 self.cancel_button = QtWidgets.QPushButton()
147 self.cancel_button.setIcon(
148 self.style().standardIcon(QtWidgets.QStyle.SP_DialogCancelButton)
150 self.cancel_button.clicked.connect(self.close)
151 buttons_layout = QtWidgets.QHBoxLayout()
152 buttons_layout.addStretch()
153 buttons_layout.addWidget(self.yes_button)
154 buttons_layout.addWidget(self.start_button)
155 buttons_layout.addWidget(self.cancel_button)
156 buttons_layout.addStretch()
159 layout = QtWidgets.QVBoxLayout()
160 layout.addWidget(self.label)
161 layout.addWidget(self.progress_bar)
162 layout.addLayout(buttons_layout)
164 central_widget = QtWidgets.QWidget()
165 central_widget.setLayout(layout)
166 self.setCentralWidget(central_widget)
170 # Set the current state of Tor Browser Launcher
171 def set_state(self, gui, message, tasks, autostart=True):
173 self.gui_message = message
174 self.gui_tasks = tasks
176 self.gui_autostart = autostart
178 # Show and hide parts of the UI based on the current state
181 self.progress_bar.hide()
182 self.yes_button.hide()
183 self.start_button.hide()
185 if "error" in self.gui:
187 self.label.setText(self.gui_message)
190 if self.gui != "error":
191 self.yes_button.setText(_("Yes"))
192 self.yes_button.show()
195 self.cancel_button.setText(_("Exit"))
197 elif self.gui == "task":
199 self.label.setText(self.gui_message)
202 self.progress_bar.show()
205 if not self.gui_autostart:
206 self.start_button.show()
209 self.cancel_button.setText(_("Cancel"))
214 if self.gui_autostart:
217 # Yes button clicked, based on the state decide what to do
218 def yes_clicked(self):
219 if self.gui == "error_try_stable":
221 elif self.gui == "error_try_default_mirror":
222 self.try_default_mirror()
223 elif self.gui == "error_try_forcing_english":
224 self.try_forcing_english()
225 elif self.gui == "error_try_tor":
228 # Start button clicked, begin tasks
229 def start(self, widget, data=None):
230 # Hide the start button
231 self.start_button.hide()
233 # Start running tasks
236 # Run the next task in the task list
238 if self.gui_task_i >= len(self.gui_tasks):
242 task = self.gui_tasks[self.gui_task_i]
244 # Get ready for the next task
247 if task == "download_version_check":
248 print(_("Downloading"), self.common.paths["version_check_url"])
251 self.common.paths["version_check_url"],
252 self.common.paths["version_check_file"],
255 if task == "set_version":
256 version = self.get_stable_version()
258 self.common.build_paths(self.get_stable_version())
259 print(_("Latest version: {}").format(version))
263 "error", _("Error detecting Tor Browser version."), [], False
267 elif task == "download_sig":
270 self.common.paths["sig_url"].format(self.common.settings["mirror"]),
273 "signature", self.common.paths["sig_url"], self.common.paths["sig_file"]
276 elif task == "download_tarball":
279 self.common.paths["tarball_url"].format(self.common.settings["mirror"]),
281 if not self.force_redownload and os.path.exists(
282 self.common.paths["tarball_file"]
288 self.common.paths["tarball_url"],
289 self.common.paths["tarball_file"],
292 elif task == "verify":
293 print(_("Verifying Signature"))
296 elif task == "extract":
297 print(_("Extracting"), self.common.paths["tarball_filename"])
301 print(_("Running"), self.common.paths["tbb"]["start"])
304 elif task == "start_over":
305 print(_("Starting download over again"))
308 def download(self, name, url, path):
309 # Download from the selected mirror
310 mirror_url = url.format(self.common.settings["mirror"]).encode()
312 # Initialize the progress bar
313 self.progress_bar.setValue(0)
314 self.progress_bar.setMaximum(100)
315 if self.common.settings["download_over_tor"]:
316 self.progress_bar.setFormat(
317 _("Downloading") + " {0} ".format(name) + _("(over Tor)") + ", %p%"
320 self.progress_bar.setFormat(_("Downloading") + " {0}, %p%".format(name))
322 def progress_update(total_bytes, bytes_so_far):
323 percent = float(bytes_so_far) / float(total_bytes)
324 amount = float(bytes_so_far)
326 for (size, unit) in [(1024 * 1024, "MiB"), (1024, "KiB")]:
329 amount /= float(size)
332 message = _("Downloaded") + (
333 " %2.1f%% (%2.1f %s)" % ((percent * 100.0), amount, units)
335 if self.common.settings["download_over_tor"]:
336 message += " " + _("(over Tor)")
338 self.progress_bar.setMaximum(total_bytes)
339 self.progress_bar.setValue(bytes_so_far)
340 self.progress_bar.setFormat(message)
342 def download_complete():
343 # Download complete, next task
346 def download_error(gui, message):
348 self.set_state(gui, message, [], False)
351 t = DownloadThread(self.common, mirror_url, path)
352 t.progress_update.connect(progress_update)
353 t.download_complete.connect(download_complete)
354 t.download_error.connect(download_error)
358 def try_default_mirror(self):
359 # change mirror to default and relaunch TBL
360 self.common.settings["mirror"] = self.common.default_mirror
361 self.common.save_settings()
362 subprocess.Popen([self.common.paths["tbl_bin"]])
365 def try_forcing_english(self):
366 # change force english to true and relaunch TBL
367 self.common.settings["force_en-US"] = True
368 self.common.save_settings()
369 subprocess.Popen([self.common.paths["tbl_bin"]])
373 # set download_over_tor to true and relaunch TBL
374 self.common.settings["download_over_tor"] = True
375 self.common.save_settings()
376 subprocess.Popen([self.common.paths["tbl_bin"]])
379 def get_stable_version(self):
380 tree = ET.parse(self.common.paths["version_check_file"])
381 for up in tree.getroot():
382 if up.tag == "update" and up.attrib["appVersion"]:
383 version = str(up.attrib["appVersion"])
385 # make sure the version does not contain directory traversal attempts
386 # e.g. "5.5.3", "6.0a", "6.0a-hardened" are valid but "../../../../.." is invalid
387 if not re.match(r"^[a-z0-9\.\-]+$", version):
394 self.progress_bar.setValue(0)
395 self.progress_bar.setMaximum(0)
396 self.progress_bar.show()
398 self.label.setText(_("Verifying Signature"))
404 # Make backup of tarball and sig
405 backup_tarball_filename = (
406 self.common.paths["tarball_file"] + ".verification_failed"
408 backup_sig_filename = self.common.paths["sig_file"] + ".verification_failed"
409 shutil.copyfile(self.common.paths["tarball_file"], backup_tarball_filename)
410 shutil.copyfile(self.common.paths["sig_file"], backup_sig_filename)
413 "SIGNATURE VERIFICATION FAILED!\n\n"
414 "Error Code: {0}\n\n"
415 "You might be under attack, there might be a network problem, or you may be missing a "
416 "recently added Tor Browser verification key.\n\n"
417 "A copy of the Tor Browser files you downloaded have been saved here:\n"
419 "Click Start to refresh the keyring and try again. If the message persists report the above "
420 "error code here:\nhttps://github.com/micahflee/torbrowser-launcher/issues"
422 sigerror = sigerror.format(
423 message, backup_tarball_filename, backup_sig_filename
426 self.set_state("task", sigerror, ["start_over"], False)
429 t = VerifyThread(self.common)
430 t.error.connect(error)
431 t.success.connect(success)
436 self.progress_bar.setValue(0)
437 self.progress_bar.setMaximum(0)
438 self.progress_bar.show()
440 self.label.setText(_("Installing"))
449 "Tor Browser Launcher doesn't understand the file format of {0}".format(
450 self.common.paths["tarball_file"]
458 t = ExtractThread(self.common)
459 t.error.connect(error)
460 t.success.connect(success)
464 def check_min_version(self):
465 installed_version = None
466 for line in open(self.common.paths["tbb"]["changelog"], "rb").readlines():
467 if line.startswith(b"Tor Browser "):
468 installed_version = line.split()[2].decode()
471 if version.parse(self.min_version) <= version.parse(installed_version):
477 # Don't run if it isn't at least the minimum version
478 if not self.check_min_version():
480 "The version of Tor Browser you have installed is earlier than it should be, which could be a "
485 Alert(self.common, message)
490 [self.common.paths["tbb"]["start"]], cwd=self.common.paths["tbb"]["dir_tbb"]
494 # Start over and download TBB again
495 def start_over(self):
496 self.force_redownload = True # Overwrite any existing file
497 self.label.setText(_("Downloading Tor Browser over again."))
498 self.gui_tasks = ["download_tarball", "verify", "extract", "run"]
502 def closeEvent(self, event):
503 # Clear the download cache
505 os.remove(self.common.paths["version_check_file"])
506 os.remove(self.common.paths["sig_file"])
507 os.remove(self.common.paths["tarball_file"])
511 super(Launcher, self).closeEvent(event)
514 class Alert(QtWidgets.QMessageBox):
523 icon=QtWidgets.QMessageBox.NoIcon,
524 buttons=QtWidgets.QMessageBox.Ok,
527 super(Alert, self).__init__(None)
529 self.setWindowTitle(_("Tor Browser Launcher"))
530 self.setWindowIcon(QtGui.QIcon(common.paths["icon_file"]))
531 self.setText(message)
533 self.setStandardButtons(buttons)
539 class DownloadThread(QtCore.QThread):
541 Download a file in a separate thread.
544 progress_update = QtCore.pyqtSignal(int, int)
545 download_complete = QtCore.pyqtSignal()
546 download_error = QtCore.pyqtSignal(str, str)
548 def __init__(self, common, url, path):
549 super(DownloadThread, self).__init__()
554 with open(self.path, "wb") as f:
559 headers={"User-Agent": "torbrowser-launcher"},
561 proxies=self.common.proxies(),
564 # If status code isn't 200, something went wrong
565 if r.status_code != 200:
566 # Should we use the default mirror?
567 if self.common.settings["mirror"] != self.common.default_mirror:
571 + _("You are currently using a non-default mirror")
573 + _("Would you like to switch back to the default?")
574 ).format(r.status_code, self.common.settings["mirror"])
575 self.download_error.emit("error_try_default_mirror", message)
577 # Should we switch to English?
579 self.common.language != "en-US"
580 and not self.common.settings["force_en-US"]
586 "Would you like to try the English version of Tor Browser instead?"
588 ).format(r.status_code)
589 self.download_error.emit("error_try_forcing_english", message)
592 message = (_("Download Error:") + " {0}").format(r.status_code)
593 self.download_error.emit("error", message)
598 # Start streaming the download
599 total_bytes = int(r.headers.get("content-length"))
601 for data in r.iter_content(chunk_size=4096):
602 bytes_so_far += len(data)
604 self.progress_update.emit(total_bytes, bytes_so_far)
606 except requests.exceptions.SSLError:
608 "Invalid SSL certificate for:\n{0}\n\nYou may be under attack."
609 ).format(self.url.decode())
610 if not self.common.settings["download_over_tor"]:
611 message += "\n\n" + _("Try the download again using Tor?")
612 self.download_error.emit("error_try_tor", message)
614 self.download_error.emit("error", message)
617 except requests.exceptions.ConnectionError:
619 if self.common.settings["download_over_tor"]:
621 "Error starting download:\n\n{0}\n\nTrying to download over Tor. "
622 "Are you sure Tor is configured correctly and running?"
623 ).format(self.url.decode())
624 self.download_error.emit("error", message)
627 "Error starting download:\n\n{0}\n\nAre you connected to the internet?"
628 ).format(self.url.decode())
629 self.download_error.emit("error", message)
633 self.download_complete.emit()
636 class VerifyThread(QtCore.QThread):
638 Verify the signature in a separate thread
641 success = QtCore.pyqtSignal()
642 error = QtCore.pyqtSignal(str)
644 def __init__(self, common):
645 super(VerifyThread, self).__init__()
649 def verify(second_try=False):
650 with gpg.Context() as c:
652 gpg.constants.protocol.OpenPGP,
653 home_dir=self.common.paths["gnupg_homedir"],
656 sig = gpg.Data(file=self.common.paths["sig_file"])
657 signed = gpg.Data(file=self.common.paths["tarball_file"])
660 c.verify(signature=sig, signed_data=signed)
661 except gpg.errors.BadSignatures as e:
663 self.error.emit(str(e))
673 # If it fails, refresh the keyring and try again
674 self.common.refresh_keyring()
678 class ExtractThread(QtCore.QThread):
680 Extract the tarball in a separate thread
683 success = QtCore.pyqtSignal()
684 error = QtCore.pyqtSignal()
686 def __init__(self, common):
687 super(ExtractThread, self).__init__()
693 if self.common.paths["tarball_file"][-2:] == "xz":
694 # if tarball is .tar.xz
695 xz = lzma.LZMAFile(self.common.paths["tarball_file"])
696 tf = tarfile.open(fileobj=xz)
697 tf.extractall(self.common.paths["tbb"]["dir"])
700 # if tarball is .tar.gz
701 if tarfile.is_tarfile(self.common.paths["tarball_file"]):
702 tf = tarfile.open(self.common.paths["tarball_file"])
703 tf.extractall(self.common.paths["tbb"]["dir"])