mirror of
https://github.com/onionshare/onionshare.git
synced 2025-02-13 05:01:23 -05:00
369 lines
12 KiB
Python
369 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
OnionShare | https://onionshare.org/
|
|
|
|
Copyright (C) 2014-2022 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,
|
|
MeekNotRunning,
|
|
):
|
|
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}")
|