onionshare/desktop/onionshare/gui_common.py

639 lines
23 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/>.
"""
import os
import shutil
from pkg_resources import resource_filename
from PySide6 import QtCore, QtWidgets, QtGui
from . import strings
from onionshare_cli.onion import (
Onion,
TorErrorInvalidSetting,
TorErrorAutomatic,
TorErrorSocketPort,
TorErrorSocketFile,
TorErrorMissingPassword,
TorErrorUnreadableCookieFile,
TorErrorAuthError,
TorErrorProtocolError,
BundledTorTimeout,
BundledTorBroken,
TorTooOldEphemeral,
TorTooOldStealth,
PortNotAvailable,
)
from onionshare_cli.meek import Meek
from onionshare_cli.web.web import WaitressException
class GuiCommon:
"""
The shared code for all of the OnionShare GUI.
"""
MODE_SHARE = "share"
MODE_RECEIVE = "receive"
MODE_WEBSITE = "website"
MODE_CHAT = "chat"
def __init__(self, common, qtapp, local_only):
self.common = common
self.qtapp = qtapp
self.local_only = local_only
# Are we running in a flatpak package?
self.is_flatpak = os.path.exists("/.flatpak-info")
# Load settings
self.common.load_settings()
# Load strings
strings.load_strings(self.common, self.get_resource_path("locale"))
# Start the Onion
self.onion = Onion(common, get_tor_paths=self.get_tor_paths)
# Lock filename
self.lock_filename = os.path.join(self.common.build_data_dir(), "lock")
# Events filenames
self.events_dir = os.path.join(self.common.build_data_dir(), "events")
if not os.path.exists(self.events_dir):
os.makedirs(self.events_dir, 0o700, True)
self.events_filename = os.path.join(self.events_dir, "events")
# Instantiate Meek, which is used to bypass censorship
self.meek = Meek(self.common, get_tor_paths=self.get_tor_paths)
self.css = self.get_css(qtapp.color_mode)
self.color_mode = qtapp.color_mode
def get_css(self, color_mode):
header_color = "#4E064F" # purple in light
title_color = "#333333" # dark gray color in main window
stop_button_color = "#d0011b" # red button color for stopping server
new_tab_button_background = "#ffffff"
new_tab_button_border = "#efeff0"
new_tab_button_text_color = "#4e0d4e"
downloads_uploads_progress_bar_border_color = "#4E064F"
downloads_uploads_progress_bar_chunk_color = "#4E064F"
share_zip_progess_bar_border_color = "#4E064F"
share_zip_progess_bar_chunk_color = "#4E064F"
history_background_color = "#ffffff"
history_label_color = "#000000"
settings_error_color = "#FF0000"
if color_mode == "dark":
header_color = "#F2F2F2"
title_color = "#F2F2F2"
stop_button_color = "#C32F2F"
new_tab_button_background = "#5F5F5F"
new_tab_button_border = "#878787"
new_tab_button_text_color = "#FFFFFF"
share_zip_progess_bar_border_color = "#F2F2F2"
history_background_color = "#191919"
history_label_color = "#ffffff"
settings_error_color = "#FF9999"
return {
# OnionShareGui styles
"tab_widget": """
QTabBar::tab { width: 170px; height: 30px; }
""",
"tab_widget_new_tab_button": """
QPushButton {
font-weight: bold;
font-size: 20px;
}""",
"settings_subtab_bar": """
QTabBar::tab {
background: transparent;
}
QTabBar::tab:selected {
border-bottom: 3px solid;
border-color: #4E064F;
padding: 3px
}""",
"mode_new_tab_button": """
QPushButton {
font-weight: bold;
font-size: 30px;
color: #601f61;
}""",
"mode_header_label": """
QLabel {
color: """
+ header_color
+ """;
font-size: 48px;
margin-bottom: 16px;
}""",
"settings_button": """
QPushButton {
border: 0;
border-radius: 0;
}""",
"server_status_indicator_label": """
QLabel {
font-style: italic;
color: #666666;
padding: 2px;
}""",
"status_bar": """
QStatusBar {
font-style: italic;
color: #666666;
}
QStatusBar::item {
border: 0px;
}""",
"autoconnect_start_button": """
QPushButton {
background-color: #5fa416;
color: #ffffff;
padding: 10px;
border: 0;
border-radius: 5px;
}""",
"autoconnect_configure_button": """
QPushButton {
padding: 9px 29px;
color: #3f7fcf;
text-align: left;
}""",
"enable_autoconnect": """
QCheckBox {
margin-top: 30px;
background: #FCFCFC;
color: #000000;
border: 1px solid #DDDBDA;
border-radius: 8px;
padding: 24px 16px;
}
QCheckBox::indicator {
width: 0;
height: 0;
}""",
"autoconnect_countries_combobox": """
QComboBox {
padding: 10px;
font-size: 16px;
margin-left: 32px;
}
QComboBox:disabled {
color: #666666;
}
""",
"autoconnect_task_label": """
QLabel {
font-weight: bold;
}
""",
"autoconnect_failed_to_connect_label": """
QLabel {
font-size: 18px;
font-weight: bold;
}""",
"autoconnect_bridge_setting_options": """
QGroupBox {
border: 0;
border-color: transparent;
background-color: transparent;
font-weight: bold;
margin-top: 16px;
}
QGroupBox::title {
subcontrol-origin: margin;
}""",
# Common styles between modes and their child widgets
"mode_settings_toggle_advanced": """
QPushButton {
color: #3f7fcf;
text-align: left;
}
""",
"mode_info_label": """
QLabel {
font-size: 12px;
color: #666666;
}
""",
"server_status_url": """
QLabel {
background-color: #ffffff;
color: #000000;
padding: 10px;
border: 1px solid #666666;
font-size: 12px;
}
""",
"server_status_url_buttons": """
QPushButton {
padding: 4px 8px;
text-align: center;
}
""",
"server_status_button_stopped": """
QPushButton {
background-color: #5fa416;
color: #ffffff;
padding: 10px 30px 10px 30px;
border: 0;
border-radius: 5px;
}""",
"server_status_button_working": """
QPushButton {
background-color: #4c8211;
color: #ffffff;
padding: 10px 30px 10px 30px;
border: 0;
border-radius: 5px;
font-style: italic;
}""",
"server_status_button_started": """
QPushButton {
background-color: """
+ stop_button_color
+ """;
color: #ffffff;
padding: 10px 30px 10px 30px;
border: 0;
border-radius: 5px;
}""",
"downloads_uploads_not_empty": """
QWidget{
background-color: """
+ history_background_color
+ """;
}""",
"downloads_uploads_empty": """
QWidget {
background-color: """
+ history_background_color
+ """;
border: 1px solid #999999;
}
QWidget QLabel {
background-color: none;
border: 0px;
}
""",
"downloads_uploads_empty_text": """
QLabel {
color: #999999;
}""",
"downloads_uploads_label": """
QLabel {
font-weight: bold;
font-size 14px;
text-align: center;
background-color: none;
border: none;
}""",
"downloads_uploads_clear": """
QPushButton {
color: #3f7fcf;
}
""",
"download_uploads_indicator": """
QLabel {
color: #ffffff;
background-color: #f44449;
font-weight: bold;
font-size: 10px;
padding: 2px;
border-radius: 7px;
text-align: center;
}""",
"downloads_uploads_progress_bar": """
QProgressBar {
border: 1px solid """
+ downloads_uploads_progress_bar_border_color
+ """;
background-color: #ffffff !important;
text-align: center;
color: #9b9b9b;
font-size: 14px;
}
QProgressBar::chunk {
background-color: """
+ downloads_uploads_progress_bar_chunk_color
+ """;
width: 10px;
}""",
"history_default_label": """
QLabel {
color: """
+ history_label_color
+ """;
}""",
"history_individual_file_timestamp_label": """
QLabel {
color: #666666;
}""",
"history_individual_file_status_code_label_2xx": """
QLabel {
color: #008800;
}""",
"history_individual_file_status_code_label_4xx": """
QLabel {
color: #cc0000;
}""",
"tor_not_connected_label": """
QLabel {
font-size: 16px;
font-style: italic;
}""",
# New tab
"new_tab_button_image": """
QLabel {
padding: 30px;
text-align: center;
}
""",
"new_tab_button_text": """
QLabel {
border: 1px solid """
+ new_tab_button_border
+ """;
border-radius: 4px;
background-color: """
+ new_tab_button_background
+ """;
text-align: center;
color: """
+ new_tab_button_text_color
+ """;
}
""",
"new_tab_title_text": """
QLabel {
text-align: center;
color: """
+ title_color
+ """;
font-size: 25px;
}
""",
# Share mode and child widget styles
"share_delete_all_files_button": """
QPushButton {
color: #3f7fcf;
}
""",
"share_zip_progess_bar": """
QProgressBar {
border: 1px solid """
+ share_zip_progess_bar_border_color
+ """;
background-color: #ffffff !important;
text-align: center;
color: #9b9b9b;
}
QProgressBar::chunk {
border: 0px;
background-color: """
+ share_zip_progess_bar_chunk_color
+ """;
width: 10px;
}""",
"share_filesize_warning": """
QLabel {
padding: 10px 0;
font-weight: bold;
color: """
+ title_color
+ """;
}
""",
"share_file_selection_drop_here_header_label": """
QLabel {
color: """
+ header_color
+ """;
font-size: 48px;
}""",
"share_file_selection_drop_here_label": """
QLabel {
color: #666666;
}""",
"share_file_selection_drop_count_label": """
QLabel {
color: #ffffff;
background-color: #f44449;
font-weight: bold;
padding: 5px 10px;
border-radius: 10px;
}""",
"share_file_list_drag_enter": """
FileList {
border: 3px solid #538ad0;
}
""",
"share_file_list_drag_leave": """
FileList {
border: none;
}
""",
"share_file_list_item_size": """
QLabel {
color: #666666;
font-size: 11px;
}""",
# Receive mode and child widget styles
"receive_file": """
QWidget {
background-color: #ffffff;
}
""",
"receive_file_size": """
QLabel {
color: #666666;
font-size: 11px;
}""",
"receive_message_button": """
QPushButton {
padding: 5px 10px;
}""",
# Tor Settings dialogs
"tor_settings_error": """
QLabel {
color: """
+ settings_error_color
+ """;
}
""",
}
def get_tor_paths(self):
if self.common.platform == "Linux":
base_path = self.get_resource_path("tor")
if base_path and os.path.isdir(base_path):
self.common.log(
"GuiCommon", "get_tor_paths", "using paths in resources"
)
tor_path = os.path.join(base_path, "tor")
tor_geo_ip_file_path = os.path.join(base_path, "geoip")
tor_geo_ipv6_file_path = os.path.join(base_path, "geoip6")
obfs4proxy_file_path = os.path.join(base_path, "obfs4proxy")
snowflake_file_path = os.path.join(base_path, "snowflake-client")
meek_client_file_path = os.path.join(base_path, "meek-client")
else:
# Fallback to looking in the path
self.common.log("GuiCommon", "get_tor_paths", "using paths from PATH")
tor_path = shutil.which("tor")
obfs4proxy_file_path = shutil.which("obfs4proxy")
snowflake_file_path = shutil.which("snowflake-client")
meek_client_file_path = shutil.which("meek-client")
prefix = os.path.dirname(os.path.dirname(tor_path))
tor_geo_ip_file_path = os.path.join(prefix, "share/tor/geoip")
tor_geo_ipv6_file_path = os.path.join(prefix, "share/tor/geoip6")
if self.common.platform == "Windows":
base_path = self.get_resource_path("tor")
tor_path = os.path.join(base_path, "tor.exe")
obfs4proxy_file_path = os.path.join(base_path, "obfs4proxy.exe")
snowflake_file_path = os.path.join(base_path, "snowflake-client.exe")
meek_client_file_path = os.path.join(base_path, "meek-client.exe")
tor_geo_ip_file_path = os.path.join(base_path, "geoip")
tor_geo_ipv6_file_path = os.path.join(base_path, "geoip6")
elif self.common.platform == "Darwin":
base_path = self.get_resource_path("tor")
tor_path = os.path.join(base_path, "tor")
obfs4proxy_file_path = os.path.join(base_path, "obfs4proxy")
snowflake_file_path = os.path.join(base_path, "snowflake-client")
meek_client_file_path = os.path.join(base_path, "meek-client")
tor_geo_ip_file_path = os.path.join(base_path, "geoip")
tor_geo_ipv6_file_path = os.path.join(base_path, "geoip6")
elif self.common.platform == "BSD":
tor_path = "/usr/local/bin/tor"
tor_geo_ip_file_path = "/usr/local/share/tor/geoip"
tor_geo_ipv6_file_path = "/usr/local/share/tor/geoip6"
obfs4proxy_file_path = "/usr/local/bin/obfs4proxy"
meek_client_file_path = "/usr/local/bin/meek-client"
snowflake_file_path = "/usr/local/bin/snowflake-client"
return (
tor_path,
tor_geo_ip_file_path,
tor_geo_ipv6_file_path,
obfs4proxy_file_path,
snowflake_file_path,
meek_client_file_path,
)
@staticmethod
def get_resource_path(filename):
"""
Returns the absolute path of a resource
"""
try:
return resource_filename("onionshare", os.path.join("resources", filename))
except KeyError:
return None
@staticmethod
def get_translated_tor_error(e):
"""
Takes an exception defined in onion.py and returns a translated error message
"""
if type(e) is TorErrorInvalidSetting:
return strings._("settings_error_unknown")
elif type(e) is TorErrorAutomatic:
return strings._("settings_error_automatic")
elif type(e) is TorErrorSocketPort:
return strings._("settings_error_socket_port").format(e.args[0], e.args[1])
elif type(e) is TorErrorSocketFile:
return strings._("settings_error_socket_file").format(e.args[0])
elif type(e) is TorErrorMissingPassword:
return strings._("settings_error_missing_password")
elif type(e) is TorErrorUnreadableCookieFile:
return strings._("settings_error_unreadable_cookie_file")
elif type(e) is TorErrorAuthError:
return strings._("settings_error_auth").format(e.args[0], e.args[1])
elif type(e) is TorErrorProtocolError:
return strings._("error_tor_protocol_error").format(e.args[0])
elif type(e) is BundledTorTimeout:
return strings._("settings_error_bundled_tor_timeout")
elif type(e) is BundledTorBroken:
return strings._("settings_error_bundled_tor_broken").format(e.args[0])
elif type(e) is TorTooOldEphemeral:
return strings._("error_ephemeral_not_supported")
elif type(e) is TorTooOldStealth:
return strings._("error_stealth_not_supported")
elif type(e) is PortNotAvailable:
return strings._("error_port_not_available")
return None
@staticmethod
def get_translated_web_error(e):
"""
Takes an exception defined in web.py and returns a translated error message
"""
if type(e) is WaitressException:
return strings._("waitress_web_server_error")
class ToggleCheckbox(QtWidgets.QCheckBox):
def __init__(self, text):
super(ToggleCheckbox, self).__init__(text)
# Set default parameters
self.setCursor(QtCore.Qt.PointingHandCursor)
self.w = 50
self.h = 24
self.bg_color = "#D4D4D4"
self.circle_color = "#BDBDBD"
self.active_color = "#4E0D4E"
self.inactive_color = ""
def hitButton(self, pos):
return self.toggleRect.contains(pos)
def paintEvent(self, e):
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.setPen(QtCore.Qt.NoPen)
opt = QtWidgets.QStyleOptionButton()
opt.initFrom(self)
self.initStyleOption(opt)
s = self.style()
s.drawControl(QtWidgets.QStyle.CE_CheckBox, opt, painter, self)
rect = QtCore.QRect(
s.subElementRect(QtWidgets.QStyle.SE_CheckBoxContents, opt, self)
)
x = (
rect.width() - rect.x() - self.w + 20
) # 20 is the padding between text and toggle
y = (
self.height() / 2 - self.h / 2 + 16
) # 16 is the padding top for the checkbox
self.toggleRect = QtCore.QRect(x, y, self.w, self.h)
painter.setBrush(QtGui.QColor(self.bg_color))
painter.drawRoundedRect(x, y, self.w, self.h, self.h / 2, self.h / 2)
if not self.isChecked():
painter.setBrush(QtGui.QColor(self.circle_color))
painter.drawEllipse(x, y - 3, self.h + 6, self.h + 6)
else:
painter.setBrush(QtGui.QColor(self.active_color))
painter.drawEllipse(
x + self.w - (self.h + 6), y - 3, self.h + 6, self.h + 6
)
painter.end()