onionshare/desktop/onionshare/connection_tab.py

726 lines
25 KiB
Python

# -*- 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/>.
"""
import json
import os
from PySide6 import QtCore, QtWidgets, QtGui
from onionshare_cli.censorship import (
CensorshipCircumvention,
CensorshipCircumventionError,
)
from onionshare_cli.meek import (
MeekNotRunning,
MeekNotFound,
)
from onionshare_cli.settings import Settings
from . import strings
from .gui_common import GuiCommon, ToggleCheckbox
from .tor_connection import TorConnectionWidget
from .update_checker import UpdateThread
from .widgets import Alert
class AutoConnectTab(QtWidgets.QWidget):
"""
Initial Tab that appears in the very beginning to ask user if
should auto connect.
"""
close_this_tab = QtCore.Signal()
tor_is_connected = QtCore.Signal()
tor_is_disconnected = QtCore.Signal()
def __init__(self, common, tab_id, status_bar, window, parent=None):
super(AutoConnectTab, self).__init__()
self.common = common
self.common.log("AutoConnectTab", "__init__")
self.status_bar = status_bar
self.tab_id = tab_id
self.window = window
self.parent = parent
# Was auto connected?
self.curr_settings = Settings(common)
self.curr_settings.load()
self.auto_connect_enabled = self.curr_settings.get("auto_connect")
# Rocket ship animation images
self.anim_stars = AnimStars(self, self.window)
self.anim_ship = AnimShip(self, self.window)
self.anim_smoke = AnimSmoke(self, self.window)
# Onionshare logo
self.image_label = QtWidgets.QLabel()
self.image_label.setPixmap(
QtGui.QPixmap.fromImage(
QtGui.QImage(
GuiCommon.get_resource_path(
os.path.join(
"images", f"{common.gui.color_mode}_logo_text_bg.png"
)
)
)
)
)
self.image_label.setFixedSize(322, 65)
image_layout = QtWidgets.QVBoxLayout()
image_layout.addWidget(self.image_label)
self.image = QtWidgets.QWidget()
self.image.setLayout(image_layout)
# First launch widget
self.first_launch_widget = AutoConnectFirstLaunchWidget(
self.common, self.curr_settings
)
self.first_launch_widget.toggle_auto_connect.connect(self.toggle_auto_connect)
self.first_launch_widget.connect_clicked.connect(
self.first_launch_widget_connect_clicked
)
self.first_launch_widget.open_tor_settings.connect(self.open_tor_settings)
self.first_launch_widget.show()
# Use bridge widget
self.use_bridge_widget = AutoConnectUseBridgeWidget(self.common)
self.use_bridge_widget.connect_clicked.connect(self.use_bridge_connect_clicked)
self.use_bridge_widget.try_again_clicked.connect(
self.first_launch_widget_connect_clicked
)
self.use_bridge_widget.open_tor_settings.connect(self.open_tor_settings)
self.use_bridge_widget.hide()
# Tor connection widget
self.tor_con = TorConnectionWidget(self.common, self.status_bar)
self.tor_con.success.connect(self.tor_con_success)
self.tor_con.fail.connect(self.tor_con_fail)
self.tor_con.update_progress.connect(self.anim_stars.update)
self.tor_con.update_progress.connect(self.anim_ship.update)
self.tor_con.update_progress.connect(self.anim_smoke.update)
self.tor_con.hide()
# Layout
content_layout = QtWidgets.QVBoxLayout()
content_layout.addStretch()
content_layout.addWidget(self.image)
content_layout.addWidget(self.first_launch_widget)
content_layout.addWidget(self.use_bridge_widget)
content_layout.addWidget(self.tor_con)
content_layout.addStretch()
content_layout.setAlignment(QtCore.Qt.AlignCenter)
content_widget = QtWidgets.QWidget()
content_widget.setLayout(content_layout)
self.layout = QtWidgets.QHBoxLayout()
self.layout.addWidget(content_widget)
self.layout.addStretch()
self.setLayout(self.layout)
def check_autoconnect(self):
"""
After rendering, check if autoconnect was clicked, then start connecting
"""
self.common.log("AutoConnectTab", "autoconnect_checking")
if self.auto_connect_enabled:
self.first_launch_widget.enable_autoconnect_checkbox.setChecked(True)
self.first_launch_widget_connect_clicked()
def toggle_auto_connect(self):
"""
Auto connect checkbox clicked
"""
self.common.log("AutoConnectTab", "autoconnect_checkbox_clicked")
self.curr_settings.set(
"auto_connect",
self.first_launch_widget.enable_autoconnect_checkbox.isChecked(),
)
self.curr_settings.save()
def open_tor_settings(self):
self.parent.open_settings_tab(from_autoconnect=True, active_tab="tor")
def first_launch_widget_connect_clicked(self):
"""
Connect button in first launch widget clicked. Try to connect to tor.
"""
self.common.log("AutoConnectTab", "first_launch_widget_connect_clicked")
self.first_launch_widget.hide_buttons()
self.tor_con.show()
self.tor_con.start(self.curr_settings)
def _got_bridges(self):
self.use_bridge_widget.progress.hide()
self.use_bridge_widget.progress_label.hide()
# Try and connect again
self.common.log(
"AutoConnectTab",
"_got_bridges",
"Got bridges. Trying to reconnect to Tor",
)
self.tor_con.show()
self.tor_con.start(self.curr_settings)
def _got_no_bridges(self):
# If we got no bridges, even after trying the default bridges
# provided by the Censorship API, try connecting again using
# our built-in obfs4 bridges
self.curr_settings.set("bridges_type", "built-in")
self.curr_settings.set("bridges_builtin_pt", "obfs4")
self.curr_settings.set("bridges_enabled", True)
self.curr_settings.save()
self._got_bridges()
def _censorship_progress_update(self, progress, summary):
self.use_bridge_widget.progress.setValue(int(progress))
self.use_bridge_widget.progress_label.setText(
f"<strong>{strings._('gui_autoconnect_circumventing_censorship')}</strong><br>{summary}"
)
def network_connection_error(self):
"""
Display an error if there simply seems no network connection.
"""
self.use_bridge_widget.connection_status_label.setText(
strings._("gui_autoconnect_failed_to_connect_to_tor")
)
self.use_bridge_widget.progress.hide()
self.use_bridge_widget.progress_label.hide()
self.use_bridge_widget.error_label.show()
self.use_bridge_widget.country_combobox.setEnabled(True)
self.use_bridge_widget.show_buttons()
self.use_bridge_widget.show()
def use_bridge_connect_clicked(self):
"""
Connect button in use bridge widget clicked.
"""
self.common.log(
"AutoConnectTab",
"use_bridge_connect_clicked",
"Trying to automatically obtain bridges",
)
self.use_bridge_widget.hide_buttons()
self.use_bridge_widget.progress.show()
self.use_bridge_widget.progress_label.show()
if self.use_bridge_widget.detect_automatic_radio.isChecked():
country = False
else:
country = self.use_bridge_widget.country_combobox.currentData().lower()
self._censorship_progress_update(
50, strings._("gui_autoconnect_circumventing_censorship_starting_meek")
)
try:
self.common.gui.meek.start()
self.censorship_circumvention = CensorshipCircumvention(
self.common, self.common.gui.meek
)
self._censorship_progress_update(
75,
strings._(
"gui_autoconnect_circumventing_censorship_requesting_bridges"
),
)
bridge_settings = self.censorship_circumvention.request_settings(
country=country
)
if not bridge_settings:
# Fall back to trying the default bridges from the API
self.common.log(
"AutoConnectTab",
"use_bridge_connect_clicked",
"Falling back to trying default bridges provided by the Censorship Circumvention API",
)
bridge_settings = (
self.censorship_circumvention.request_default_bridges()
)
self.common.gui.meek.cleanup()
if bridge_settings and self.censorship_circumvention.save_settings(
self.curr_settings, bridge_settings
):
self._censorship_progress_update(
100,
strings._("gui_autoconnect_circumventing_censorship_got_bridges"),
)
self._got_bridges()
else:
self._got_no_bridges()
except (
MeekNotRunning,
MeekNotFound,
) as e:
self._got_no_bridges()
except CensorshipCircumventionError as e:
self.common.log(
"AutoConnectTab",
"use_bridge_connect_clicked",
"Request to the Tor Censorship Circumvention API failed. No network connection?",
)
self.network_connection_error()
def check_for_updates(self):
"""
Check for OnionShare updates in a new thread, if enabled.
"""
if self.common.platform == "Windows" or self.common.platform == "Darwin":
if self.common.settings.get("use_autoupdate"):
def update_available(update_url, installed_version, latest_version):
Alert(
self.common,
strings._("update_available").format(
update_url, installed_version, latest_version
),
)
self.update_thread = UpdateThread(self.common, self.common.gui.onion)
self.update_thread.update_available.connect(update_available)
self.update_thread.start()
def tor_con_success(self):
"""
Finished testing tor connection.
"""
self.tor_con.hide()
self.first_launch_widget.show_buttons()
self.use_bridge_widget.show_buttons()
self.use_bridge_widget.progress.hide()
self.use_bridge_widget.progress_label.hide()
if self.common.gui.onion.is_authenticated() and not self.tor_con.wasCanceled():
# Tell the tabs that Tor is connected
self.tor_is_connected.emit()
# After connecting to Tor, check for updates
self.check_for_updates()
# Close the tab
self.close_this_tab.emit()
def tor_con_fail(self, msg):
"""
Finished testing tor connection.
"""
self.tor_con.hide()
# If there is a message, update the text of the bridge widget
if msg:
self.use_bridge_widget.connection_error_message.setText(msg)
# If we're on first launch, check if wasCanceled
# If cancelled, stay in first launch widget and show buttons
# Else, switch to use bridge
if self.first_launch_widget.isVisible():
if self.tor_con.wasCanceled():
self.first_launch_widget.show_buttons()
else:
self.first_launch_widget.show_buttons()
self.first_launch_widget.hide()
self.use_bridge_widget.show()
else:
self.use_bridge_widget.show_buttons()
def reload_settings(self):
"""
Reload the latest Tor settings, and reset to show the
first-launch widget if it had been hidden.
"""
self.curr_settings.load()
self.auto_connect_enabled = self.curr_settings.get("auto_connect")
self.first_launch_widget.enable_autoconnect_checkbox.setChecked(
self.auto_connect_enabled
)
self.use_bridge_widget.hide()
self.first_launch_widget.show_buttons()
self.first_launch_widget.show()
class Anim(QtWidgets.QLabel):
"""
Rocket ship animation base class
"""
force_update = QtCore.Signal(int)
def __init__(self, parent, window, w, h, filename):
super(Anim, self).__init__(parent=parent)
self.window = window
self.window.window_resized.connect(self.update_same_percent)
self.w = w
self.h = h
self.percent = 0
self.used_percentages = []
self.setPixmap(
QtGui.QPixmap.fromImage(
QtGui.QImage(
GuiCommon.get_resource_path(os.path.join("images", filename))
)
)
)
self.setFixedSize(self.w, self.h)
self.update(0)
self.force_update.connect(self.update)
def update_same_percent(self):
self.update(self.percent)
def update(self, percent):
self.percent = percent
self.move()
self.setGeometry(int(self.x), int(self.y), int(self.w), int(self.h))
def move(self):
# Implement in child
pass
class AnimStars(Anim):
"""
Rocket ship animation part: stars
"""
def __init__(self, parent, window):
super(AnimStars, self).__init__(
parent, window, 740, 629, "tor-connect-stars.png"
)
def move(self):
self.x = self.window.width() - self.w
self.y = 0
# Stars don't move until 10%, then move down
if self.percent >= 10:
self.y += self.percent * 6.6
class AnimShip(Anim):
"""
Rocket ship animation part: ship
"""
def __init__(self, parent, window):
super(AnimShip, self).__init__(parent, window, 239, 545, "tor-connect-ship.png")
def move(self):
self.x = self.window.width() - self.w - 150
self.y = self.window.height() - self.h - 40
# Ship moves up
self.y -= self.percent * 6.6
class AnimSmoke(Anim):
"""
Rocket ship animation part: smoke
"""
def __init__(self, parent, window):
super(AnimSmoke, self).__init__(
parent, window, 522, 158, "tor-connect-smoke.png"
)
def move(self):
self.x = self.window.width() - self.w
self.y = self.window.height() - self.h + 50
# Smoke moves up until 50%, then moves down
self.y -= self.percent * 6.6
if self.percent >= 50:
self.y += self.percent * 6.7
class AutoConnectFirstLaunchWidget(QtWidgets.QWidget):
"""
When you first launch OnionShare, this is the widget that is displayed
"""
toggle_auto_connect = QtCore.Signal()
connect_clicked = QtCore.Signal()
open_tor_settings = QtCore.Signal()
def __init__(self, common, settings):
super(AutoConnectFirstLaunchWidget, self).__init__()
self.common = common
self.common.log("AutoConnectFirstLaunchWidget", "__init__")
self.settings = settings
# Description and checkbox
description_label = QtWidgets.QLabel(strings._("gui_autoconnect_description"))
self.enable_autoconnect_checkbox = ToggleCheckbox(
strings._("gui_enable_autoconnect_checkbox")
)
self.enable_autoconnect_checkbox.setChecked(self.settings.get("auto_connect"))
self.enable_autoconnect_checkbox.clicked.connect(self._toggle_auto_connect)
self.enable_autoconnect_checkbox.setFixedWidth(400)
self.enable_autoconnect_checkbox.setStyleSheet(
common.gui.css["enable_autoconnect"]
)
description_layout = QtWidgets.QVBoxLayout()
description_layout.addWidget(description_label)
description_layout.addWidget(self.enable_autoconnect_checkbox)
description_widget = QtWidgets.QWidget()
description_widget.setLayout(description_layout)
# Buttons
self.connect_button = QtWidgets.QPushButton(strings._("gui_autoconnect_start"))
self.connect_button.clicked.connect(self._connect_clicked)
self.connect_button.setFixedWidth(150)
self.connect_button.setStyleSheet(common.gui.css["autoconnect_start_button"])
self.configure_button = QtWidgets.QPushButton(
strings._("gui_autoconnect_configure")
)
self.configure_button.clicked.connect(self._open_tor_settings)
self.configure_button.setFlat(True)
self.configure_button.setStyleSheet(
common.gui.css["autoconnect_configure_button"]
)
cta_layout = QtWidgets.QHBoxLayout()
cta_layout.addWidget(self.connect_button)
cta_layout.addWidget(self.configure_button)
cta_widget = QtWidgets.QWidget()
cta_widget.setLayout(cta_layout)
# Layout
layout = QtWidgets.QVBoxLayout()
layout.addWidget(description_widget)
layout.addWidget(cta_widget)
self.setLayout(layout)
def hide_buttons(self):
self.connect_button.hide()
self.configure_button.hide()
def show_buttons(self):
self.connect_button.show()
self.configure_button.show()
def _toggle_auto_connect(self):
self.toggle_auto_connect.emit()
def _connect_clicked(self):
self.connect_clicked.emit()
def _open_tor_settings(self):
self.open_tor_settings.emit()
class AutoConnectUseBridgeWidget(QtWidgets.QWidget):
"""
If connecting fails, this is the widget that helps the user bypass censorship
"""
connect_clicked = QtCore.Signal()
try_again_clicked = QtCore.Signal()
open_tor_settings = QtCore.Signal()
def __init__(self, common):
super(AutoConnectUseBridgeWidget, self).__init__()
self.common = common
self.common.log("AutoConnectUseBridgeWidget", "__init__")
# Heading label when we fail to connect to Tor.
self.connection_status_label = QtWidgets.QLabel(
strings._("gui_autoconnect_failed_to_connect_to_tor")
)
self.connection_status_label.setTextFormat(QtCore.Qt.RichText)
self.connection_status_label.setStyleSheet(
common.gui.css["autoconnect_failed_to_connect_label"]
)
# Tor connection error message
self.connection_error_message = QtWidgets.QLabel(
strings._("gui_autoconnect_connection_error_msg")
)
self.connection_error_message.setTextFormat(QtCore.Qt.RichText)
self.connection_error_message.setWordWrap(True)
self.connection_error_message.setContentsMargins(0, 0, 0, 10)
# Description
self.description_label = QtWidgets.QLabel(
strings._("gui_autoconnect_bridge_description")
)
self.description_label.setTextFormat(QtCore.Qt.RichText)
self.description_label.setWordWrap(True)
self.description_label.setContentsMargins(0, 0, 0, 20)
# Detection preference
self.use_bridge = True
self.no_bridge = QtWidgets.QRadioButton(
strings._("gui_autoconnect_no_bridge")
)
self.no_bridge.toggled.connect(self._toggle_no_bridge)
self.detect_automatic_radio = QtWidgets.QRadioButton(
strings._("gui_autoconnect_bridge_detect_automatic")
)
self.detect_automatic_radio.toggled.connect(self._detect_automatic_toggled)
self.detect_manual_radio = QtWidgets.QRadioButton(
strings._("gui_autoconnect_bridge_detect_manual")
)
self.detect_manual_radio.toggled.connect(self._detect_manual_toggled)
detect_layout = QtWidgets.QVBoxLayout()
detect_layout.addWidget(self.no_bridge)
detect_layout.addWidget(self.detect_automatic_radio)
detect_layout.addWidget(self.detect_manual_radio)
bridge_setting_options = QtWidgets.QGroupBox(
strings._("gui_autoconnect_bridge_setting_options")
)
bridge_setting_options.setLayout(detect_layout)
bridge_setting_options.setFlat(True)
bridge_setting_options.setStyleSheet(
common.gui.css["autoconnect_bridge_setting_options"]
)
# Country list
locale = self.common.settings.get("locale")
if not locale:
locale = "en"
with open(
GuiCommon.get_resource_path(os.path.join("countries", f"{locale}.json"))
) as f:
countries = json.loads(f.read())
self.country_combobox = QtWidgets.QComboBox()
self.country_combobox.setStyleSheet(
common.gui.css["autoconnect_countries_combobox"]
)
self.country_combobox.setIconSize(QtCore.QSize(26, 20))
for country_code in countries:
icon = QtGui.QIcon(
GuiCommon.get_resource_path(os.path.join("images", "countries", f"{country_code.lower()}.png"))
)
self.country_combobox.addItem(icon, countries[country_code], country_code)
# Task label
self.task_label = QtWidgets.QLabel()
self.task_label.setStyleSheet(common.gui.css["autoconnect_task_label"])
self.task_label.setAlignment(QtCore.Qt.AlignCenter)
self.task_label.hide()
# Buttons
self.connect_button = QtWidgets.QPushButton(
strings._("gui_autoconnect_start")
)
self.connect_button.clicked.connect(self._connect_clicked)
self.connect_button.setFixedWidth(150)
self.connect_button.setStyleSheet(common.gui.css["autoconnect_start_button"])
self.configure_button = QtWidgets.QPushButton(
strings._("gui_autoconnect_configure")
)
self.configure_button.clicked.connect(self._open_tor_settings)
self.configure_button.setFlat(True)
self.configure_button.setStyleSheet(
common.gui.css["autoconnect_configure_button"]
)
# Error label
self.error_label = QtWidgets.QLabel(
strings._("gui_autoconnect_could_not_connect_to_tor_api")
)
self.error_label.setStyleSheet(self.common.gui.css["tor_settings_error"])
self.error_label.setWordWrap(True)
self.error_label.hide()
self.progress = QtWidgets.QProgressBar()
self.progress.setRange(0, 100)
self.progress_label = QtWidgets.QLabel(
strings._("gui_autoconnect_circumventing_censorship")
)
self.progress_label.setAlignment(QtCore.Qt.AlignHCenter)
self.progress.hide()
self.progress_label.hide()
cta_layout = QtWidgets.QHBoxLayout()
cta_layout.addWidget(self.connect_button)
cta_layout.addWidget(self.configure_button)
cta_layout.addStretch()
cta_widget = QtWidgets.QWidget()
cta_widget.setLayout(cta_layout)
# Layout
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.connection_status_label)
layout.addWidget(self.connection_error_message)
layout.addWidget(self.description_label)
layout.addWidget(bridge_setting_options)
layout.addWidget(self.country_combobox)
layout.addWidget(self.task_label)
layout.addWidget(cta_widget)
layout.addWidget(self.progress)
layout.addWidget(self.progress_label)
layout.addWidget(self.error_label)
self.setLayout(layout)
self.detect_automatic_radio.setChecked(True)
def hide_buttons(self):
self.connect_button.hide()
self.configure_button.hide()
self.connection_error_message.hide()
self.description_label.hide()
self.error_label.hide()
self.no_bridge.hide()
self.detect_automatic_radio.hide()
self.detect_manual_radio.hide()
def show_buttons(self):
self.connect_button.show()
self.connection_error_message.show()
self.description_label.show()
self.configure_button.show()
self.no_bridge.show()
self.detect_automatic_radio.show()
self.detect_manual_radio.show()
def _toggle_no_bridge(self):
self.use_bridge = not self.use_bridge
def _detect_automatic_toggled(self):
self.country_combobox.setEnabled(False)
self.country_combobox.hide()
def _detect_manual_toggled(self):
self.country_combobox.setEnabled(True)
self.country_combobox.show()
def _connect_clicked(self):
self.country_combobox.setEnabled(False)
self.hide_buttons()
self.connection_status_label.setText(
strings._("gui_autoconnect_trying_to_connect_to_tor")
)
print(self.use_bridge)
if not self.use_bridge:
self.try_again_clicked.emit()
else:
self.connect_clicked.emit()
def _open_tor_settings(self):
self.open_tor_settings.emit()