Actually get bridges from moat

This commit is contained in:
Micah Lee 2021-10-17 14:02:11 -07:00
parent 6bf839f826
commit 2212589625
No known key found for this signature in database
GPG Key ID: 403C2657CD994F73
4 changed files with 210 additions and 49 deletions

View File

@ -392,6 +392,12 @@ class GuiCommon:
QPushButton { QPushButton {
padding: 5px 10px; padding: 5px 10px;
}""", }""",
# Moat dialog
"moat_error": """
QLabel {
color: #990000;
}
""",
} }
def get_tor_paths(self): def get_tor_paths(self):

View File

@ -20,6 +20,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
from PySide2 import QtCore, QtWidgets, QtGui from PySide2 import QtCore, QtWidgets, QtGui
import requests import requests
import os
import base64
from . import strings from . import strings
from .gui_common import GuiCommon from .gui_common import GuiCommon
@ -30,6 +32,8 @@ class MoatDialog(QtWidgets.QDialog):
Moat dialog: Request a bridge from torproject.org Moat dialog: Request a bridge from torproject.org
""" """
got_bridges = QtCore.Signal(str)
def __init__(self, common): def __init__(self, common):
super(MoatDialog, self).__init__() super(MoatDialog, self).__init__()
@ -42,7 +46,7 @@ class MoatDialog(QtWidgets.QDialog):
self.setWindowIcon(QtGui.QIcon(GuiCommon.get_resource_path("images/logo.png"))) self.setWindowIcon(QtGui.QIcon(GuiCommon.get_resource_path("images/logo.png")))
# Label # Label
self.label = QtWidgets.QLabel(strings._("moat_contact_label")) self.label = QtWidgets.QLabel()
# CAPTCHA image # CAPTCHA image
self.captcha = QtWidgets.QLabel() self.captcha = QtWidgets.QLabel()
@ -50,7 +54,7 @@ class MoatDialog(QtWidgets.QDialog):
# Solution input # Solution input
self.solution_lineedit = QtWidgets.QLineEdit() self.solution_lineedit = QtWidgets.QLineEdit()
self.solution_lineedit.editingFinished.connect(self.solution_editing_finished) self.solution_lineedit.setPlaceholderText(strings._("moat_captcha_placeholder"))
self.reload_button = QtWidgets.QPushButton(strings._("moat_captcha_reload")) self.reload_button = QtWidgets.QPushButton(strings._("moat_captcha_reload"))
self.reload_button.clicked.connect(self.reload_clicked) self.reload_button.clicked.connect(self.reload_clicked)
solution_layout = QtWidgets.QHBoxLayout() solution_layout = QtWidgets.QHBoxLayout()
@ -59,12 +63,12 @@ class MoatDialog(QtWidgets.QDialog):
# Error label # Error label
self.error_label = QtWidgets.QLabel() self.error_label = QtWidgets.QLabel()
self.error_label.setStyleSheet(self.common.gui.css["moat_error"])
self.error_label.hide() self.error_label.hide()
# Buttons # Buttons
self.submit_button = QtWidgets.QPushButton(strings._("moat_captcha_submit")) self.submit_button = QtWidgets.QPushButton(strings._("moat_captcha_submit"))
self.submit_button.clicked.connect(self.submit_clicked) self.submit_button.clicked.connect(self.submit_clicked)
self.submit_button.setEnabled(False)
self.cancel_button = QtWidgets.QPushButton( self.cancel_button = QtWidgets.QPushButton(
strings._("gui_settings_button_cancel") strings._("gui_settings_button_cancel")
) )
@ -79,6 +83,7 @@ class MoatDialog(QtWidgets.QDialog):
layout.addWidget(self.label) layout.addWidget(self.label)
layout.addWidget(self.captcha) layout.addWidget(self.captcha)
layout.addLayout(solution_layout) layout.addLayout(solution_layout)
layout.addStretch()
layout.addWidget(self.error_label) layout.addWidget(self.error_label)
layout.addLayout(buttons_layout) layout.addLayout(buttons_layout)
@ -86,35 +91,95 @@ class MoatDialog(QtWidgets.QDialog):
self.cancel_button.setFocus() self.cancel_button.setFocus()
self.reload_clicked() self.reload_clicked()
self.exec_()
def solution_editing_finished(self):
"""
Finished typing something in the solution field.
"""
self.common.log("MoatDialog", "solution_editing_finished")
pass
def reload_clicked(self): def reload_clicked(self):
""" """
Reload button clicked. Reload button clicked.
""" """
self.common.log("MoatDialog", "reload_clicked") self.common.log("MoatDialog", "reload_clicked")
pass
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, "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): def submit_clicked(self):
""" """
Submit button clicked. Submit button clicked.
""" """
self.common.log("MoatDialog", "submit_clicked") self.error_label.hide()
pass
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,
"check",
{"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): def cancel_clicked(self):
""" """
Cancel button clicked. Cancel button clicked.
""" """
self.common.log("MoatDialog", "cancel_clicked") self.common.log("MoatDialog", "cancel_clicked")
pass self.close()
def bridgedb_error(self):
self.common.log("MoatDialog", "bridgedb_error")
self.error_label.setText(strings._("moat_bridgedb_error"))
self.error_label.show()
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()
def captcha_ready(self, image, challenge):
self.common.log("MoatDialog", "captcha_ready")
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.show()
self.reload_button.show()
self.submit_button.show()
def bridges_ready(self, bridges):
self.common.log("MoatDialog", "bridges_ready", bridges)
self.got_bridges.emit(bridges)
self.close()
class MoatThread(QtCore.QThread): class MoatThread(QtCore.QThread):
@ -127,21 +192,26 @@ class MoatThread(QtCore.QThread):
""" """
tor_status_update = QtCore.Signal(str, str) bridgedb_error = QtCore.Signal()
captcha_error = QtCore.Signal(str)
captcha_ready = QtCore.Signal(str, str)
bridges_ready = QtCore.Signal(str)
def __init__(self, common, action, data): def __init__(self, common, action, data={}):
super(MoatThread, self).__init__() super(MoatThread, self).__init__()
self.common = common self.common = common
self.common.log("MoatThread", "__init__", f"action={action}") self.common.log("MoatThread", "__init__", f"action={action}")
self.transport = "obfs4"
self.action = action self.action = action
self.data = data self.data = data
def run(self): def run(self):
self.common.log("MoatThread", "run")
# TODO: Do all of this using domain fronting # TODO: Do all of this using domain fronting
if self.action == "fetch":
self.common.log("MoatThread", "run", f"starting fetch")
# Request a bridge # Request a bridge
r = requests.post( r = requests.post(
"https://bridges.torproject.org/moat/fetch", "https://bridges.torproject.org/moat/fetch",
@ -151,24 +221,97 @@ class MoatThread(QtCore.QThread):
{ {
"version": "0.1.0", "version": "0.1.0",
"type": "client-transports", "type": "client-transports",
"supported": ["obfs4"], "supported": [self.transport],
} }
] ]
}, },
) )
if r.status_code != 200: if r.status_code != 200:
return moat_error() self.common.log("MoatThread", "run", f"status_code={r.status_code}")
self.bridgedb_error.emit()
return
try: try:
moat_res = r.json() moat_res = r.json()
if "errors" in moat_res or "data" not in moat_res: if "errors" in moat_res:
return moat_error() self.common.log("MoatThread", "run", f"errors={moat_res['errors']}")
if moat_res["type"] != "moat-challenge": self.bridgedb_error.emit()
return moat_error() 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
if moat_res["data"][0]["transport"] != self.transport:
self.common.log(
"MoatThread", "run", f"transport != {self.transport}"
)
self.bridgedb_error.emit()
return
moat_type = moat_res["type"] image = moat_res["data"][0]["image"]
moat_transport = moat_res["transport"] challenge = moat_res["data"][0]["challenge"]
moat_image = moat_res["image"]
moat_challenge = moat_res["challenge"] self.captcha_ready.emit(image, challenge)
except: except Exception as e:
return moat_error() 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"},
json={
"data": [
{
"id": "2",
"type": "moat-solution",
"version": "0.1.0",
"transport": self.transport,
"challenge": self.data["challenge"],
"solution": self.data["solution"],
"qrcode": "false",
}
]
},
)
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']}")
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}")

View File

@ -226,5 +226,6 @@
"moat_captcha_submit": "Submit", "moat_captcha_submit": "Submit",
"moat_captcha_reload": "Reload", "moat_captcha_reload": "Reload",
"moat_bridgedb_error": "Error contacting BridgeDB.", "moat_bridgedb_error": "Error contacting BridgeDB.",
"moat_captcha_error": "The solution is not correct. Please try again." "moat_captcha_error": "The solution is not correct. Please try again.",
"moat_solution_empty_error": "You must enter the characters from the image"
} }

View File

@ -148,7 +148,8 @@ class TorSettingsDialog(QtWidgets.QDialog):
self.bridge_moat_button.clicked.connect(self.bridge_moat_button_clicked) self.bridge_moat_button.clicked.connect(self.bridge_moat_button_clicked)
self.bridge_moat_textbox = QtWidgets.QPlainTextEdit() self.bridge_moat_textbox = QtWidgets.QPlainTextEdit()
self.bridge_moat_textbox.setMaximumHeight(100) self.bridge_moat_textbox.setMaximumHeight(100)
self.bridge_moat_textbox.setEnabled(False) self.bridge_moat_textbox.setReadOnly(True)
self.bridge_moat_textbox.setWordWrapMode(QtGui.QTextOption.NoWrap)
self.bridge_moat_textbox.hide() self.bridge_moat_textbox.hide()
bridge_moat_textbox_options_layout = QtWidgets.QVBoxLayout() bridge_moat_textbox_options_layout = QtWidgets.QVBoxLayout()
bridge_moat_textbox_options_layout.addWidget(self.bridge_moat_button) bridge_moat_textbox_options_layout.addWidget(self.bridge_moat_button)
@ -508,6 +509,16 @@ class TorSettingsDialog(QtWidgets.QDialog):
self.common.log("TorSettingsDialog", "bridge_moat_button_clicked") self.common.log("TorSettingsDialog", "bridge_moat_button_clicked")
moat_dialog = MoatDialog(self.common) moat_dialog = MoatDialog(self.common)
moat_dialog.got_bridges.connect(self.bridge_moat_got_bridges)
moat_dialog.exec_()
def bridge_moat_got_bridges(self, bridges):
"""
Got new bridges from moat
"""
self.common.log("TorSettingsDialog", "bridge_moat_got_bridges")
self.bridge_moat_textbox.document().setPlainText(bridges)
self.bridge_moat_textbox.show()
def bridge_custom_radio_toggled(self, checked): def bridge_custom_radio_toggled(self, checked):
""" """