diff options
Diffstat (limited to 'desktop/onionshare/moat_dialog.py')
-rw-r--r-- | desktop/onionshare/moat_dialog.py | 372 |
1 files changed, 372 insertions, 0 deletions
diff --git a/desktop/onionshare/moat_dialog.py b/desktop/onionshare/moat_dialog.py new file mode 100644 index 00000000..84a52390 --- /dev/null +++ b/desktop/onionshare/moat_dialog.py @@ -0,0 +1,372 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2014-2021 Micah Lee, et al. <micah@micahflee.com> + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see <http://www.gnu.org/licenses/>. +""" + +from PySide2 import QtCore, QtWidgets, QtGui +import requests +import os +import base64 +import json + +from . import strings +from .gui_common import GuiCommon +from onionshare_cli.meek import MeekNotFound, MeekNotRunning + + +class MoatDialog(QtWidgets.QDialog): + """ + Moat dialog: Request a bridge from torproject.org + """ + + got_bridges = QtCore.Signal(str) + + def __init__(self, common, meek): + super(MoatDialog, self).__init__() + + self.common = common + + self.common.log("MoatDialog", "__init__") + + self.meek = meek + + self.setModal(True) + self.setWindowTitle(strings._("gui_settings_bridge_moat_button")) + self.setWindowIcon(QtGui.QIcon(GuiCommon.get_resource_path("images/logo.png"))) + + # Label + self.label = QtWidgets.QLabel() + + # CAPTCHA image + self.captcha = QtWidgets.QLabel() + self.captcha.setFixedSize(400, 125) # this is the size of the CAPTCHA image + + # Solution input + self.solution_lineedit = QtWidgets.QLineEdit() + self.solution_lineedit.setPlaceholderText(strings._("moat_captcha_placeholder")) + self.solution_lineedit.editingFinished.connect( + self.solution_lineedit_editing_finished + ) + self.submit_button = QtWidgets.QPushButton(strings._("moat_captcha_submit")) + self.submit_button.clicked.connect(self.submit_clicked) + solution_layout = QtWidgets.QHBoxLayout() + solution_layout.addWidget(self.solution_lineedit) + solution_layout.addWidget(self.submit_button) + + # Error label + self.error_label = QtWidgets.QLabel() + self.error_label.setStyleSheet(self.common.gui.css["tor_settings_error"]) + self.error_label.hide() + + # Buttons + self.reload_button = QtWidgets.QPushButton(strings._("moat_captcha_reload")) + self.reload_button.clicked.connect(self.reload_clicked) + self.cancel_button = QtWidgets.QPushButton( + strings._("gui_settings_button_cancel") + ) + self.cancel_button.clicked.connect(self.cancel_clicked) + buttons_layout = QtWidgets.QHBoxLayout() + buttons_layout.addStretch() + buttons_layout.addWidget(self.reload_button) + buttons_layout.addWidget(self.cancel_button) + + # Layout + layout = QtWidgets.QVBoxLayout() + layout.addWidget(self.label) + layout.addWidget(self.captcha) + layout.addLayout(solution_layout) + layout.addStretch() + layout.addWidget(self.error_label) + layout.addLayout(buttons_layout) + + self.setLayout(layout) + self.cancel_button.setFocus() + + self.reload_clicked() + + def reload_clicked(self): + """ + Reload button clicked. + """ + self.common.log("MoatDialog", "reload_clicked") + + self.label.setText(strings._("moat_contact_label")) + self.error_label.hide() + + self.captcha.hide() + self.solution_lineedit.hide() + self.reload_button.hide() + self.submit_button.hide() + + # BridgeDB fetch + self.t_fetch = MoatThread(self.common, self.meek, "fetch") + self.t_fetch.bridgedb_error.connect(self.bridgedb_error) + self.t_fetch.captcha_ready.connect(self.captcha_ready) + self.t_fetch.start() + + def submit_clicked(self): + """ + Submit button clicked. + """ + self.error_label.hide() + self.solution_lineedit.setEnabled(False) + + solution = self.solution_lineedit.text().strip() + if len(solution) == 0: + self.common.log("MoatDialog", "submit_clicked", "solution is blank") + self.error_label.setText(strings._("moat_solution_empty_error")) + self.error_label.show() + return + + # BridgeDB check + self.t_check = MoatThread( + self.common, + self.meek, + "check", + { + "transport": self.transport, + "challenge": self.challenge, + "solution": self.solution_lineedit.text(), + }, + ) + self.t_check.bridgedb_error.connect(self.bridgedb_error) + self.t_check.captcha_error.connect(self.captcha_error) + self.t_check.bridges_ready.connect(self.bridges_ready) + self.t_check.start() + + def cancel_clicked(self): + """ + Cancel button clicked. + """ + self.common.log("MoatDialog", "cancel_clicked") + self.close() + + def bridgedb_error(self): + self.common.log("MoatDialog", "bridgedb_error") + self.error_label.setText(strings._("moat_bridgedb_error")) + self.error_label.show() + + self.solution_lineedit.setEnabled(True) + + def captcha_error(self, msg): + self.common.log("MoatDialog", "captcha_error") + if msg == "": + self.error_label.setText(strings._("moat_captcha_error")) + else: + self.error_label.setText(msg) + self.error_label.show() + + self.solution_lineedit.setEnabled(True) + + def captcha_ready(self, transport, image, challenge): + self.common.log("MoatDialog", "captcha_ready") + + self.transport = transport + self.challenge = challenge + + # Save captcha image to disk, so we can load it + captcha_data = base64.b64decode(image) + captcha_filename = os.path.join(self.common.build_tmp_dir(), "captcha.jpg") + with open(captcha_filename, "wb") as f: + f.write(captcha_data) + + self.captcha.setPixmap(QtGui.QPixmap.fromImage(QtGui.QImage(captcha_filename))) + os.remove(captcha_filename) + + self.label.setText(strings._("moat_captcha_label")) + self.captcha.show() + self.solution_lineedit.setEnabled(True) + self.solution_lineedit.setText("") + self.solution_lineedit.show() + self.solution_lineedit.setFocus() + self.reload_button.show() + self.submit_button.show() + + def solution_lineedit_editing_finished(self): + self.common.log("MoatDialog", "solution_lineedit_editing_finished") + + def bridges_ready(self, bridges): + self.common.log("MoatDialog", "bridges_ready", bridges) + self.got_bridges.emit(bridges) + self.close() + + +class MoatThread(QtCore.QThread): + """ + This does all of the communicating with BridgeDB in a separate thread. + + Valid actions are: + - "fetch": requests a new CAPTCHA + - "check": sends a CAPTCHA solution + + """ + + bridgedb_error = QtCore.Signal() + captcha_error = QtCore.Signal(str) + captcha_ready = QtCore.Signal(str, str, str) + bridges_ready = QtCore.Signal(str) + + def __init__(self, common, meek, action, data={}): + super(MoatThread, self).__init__() + self.common = common + self.common.log("MoatThread", "__init__", f"action={action}") + + self.meek = meek + self.transport = "obfs4" + self.action = action + self.data = data + + def run(self): + + # Start Meek so that we can do domain fronting + try: + self.meek.start() + except MeekNotFound: + self.common.log("MoatThread", "run", f"Could not find meek-client") + self.bridgedb_error.emit() + return + except MeekNotRunning: + self.common.log( + "MoatThread", "run", f"Ran meek-client, but there was an error" + ) + self.bridgedb_error.emit() + return + + # We should only fetch bridges if we can domain front, + # but we can override this in local-only mode. + if not self.meek.meek_proxies and not self.common.gui.local_only: + self.common.log( + "MoatThread", "run", f"Could not identify meek proxies to make request" + ) + self.bridgedb_error.emit() + return + + if self.action == "fetch": + self.common.log("MoatThread", "run", f"starting fetch") + + # Request a bridge + r = requests.post( + "https://bridges.torproject.org/moat/fetch", + headers={"Content-Type": "application/vnd.api+json"}, + proxies=self.meek.meek_proxies, + json={ + "data": [ + { + "version": "0.1.0", + "type": "client-transports", + "supported": ["obfs4", "snowflake"], + } + ] + }, + ) + + self.meek.cleanup() + + if r.status_code != 200: + self.common.log("MoatThread", "run", f"status_code={r.status_code}") + self.bridgedb_error.emit() + return + + try: + moat_res = r.json() + if "errors" in moat_res: + self.common.log("MoatThread", "run", f"errors={moat_res['errors']}") + self.bridgedb_error.emit() + return + if "data" not in moat_res: + self.common.log("MoatThread", "run", f"no data") + self.bridgedb_error.emit() + return + if moat_res["data"][0]["type"] != "moat-challenge": + self.common.log("MoatThread", "run", f"type != moat-challange") + self.bridgedb_error.emit() + return + + transport = moat_res["data"][0]["transport"] + image = moat_res["data"][0]["image"] + challenge = moat_res["data"][0]["challenge"] + + self.captcha_ready.emit(transport, image, challenge) + except Exception as e: + self.common.log("MoatThread", "run", f"hit exception: {e}") + self.bridgedb_error.emit() + return + + elif self.action == "check": + self.common.log("MoatThread", "run", f"starting check") + + # Check the CAPTCHA + r = requests.post( + "https://bridges.torproject.org/moat/check", + headers={"Content-Type": "application/vnd.api+json"}, + proxies=self.meek.meek_proxies, + json={ + "data": [ + { + "id": "2", + "type": "moat-solution", + "version": "0.1.0", + "transport": self.data["transport"], + "challenge": self.data["challenge"], + "solution": self.data["solution"], + "qrcode": "false", + } + ] + }, + ) + + self.meek.cleanup() + + if r.status_code != 200: + self.common.log("MoatThread", "run", f"status_code={r.status_code}") + self.bridgedb_error.emit() + return + + try: + moat_res = r.json() + self.common.log( + "MoatThread", + "run", + f"got bridges:\n{json.dumps(moat_res,indent=2)}", + ) + + if "errors" in moat_res: + self.common.log("MoatThread", "run", f"errors={moat_res['errors']}") + if moat_res["errors"][0]["code"] == 419: + self.captcha_error.emit("") + return + else: + errors = " ".join([e["detail"] for e in moat_res["errors"]]) + self.captcha_error.emit(errors) + return + + if moat_res["data"][0]["type"] != "moat-bridges": + self.common.log("MoatThread", "run", f"type != moat-bridges") + self.bridgedb_error.emit() + return + + bridges = moat_res["data"][0]["bridges"] + self.bridges_ready.emit("\n".join(bridges)) + + except Exception as e: + self.common.log("MoatThread", "run", f"hit exception: {e}") + self.bridgedb_error.emit() + return + + else: + self.common.log("MoatThread", "run", f"invalid action: {self.action}") |