forked from Support/sp-hydra-veil-gui
307 lines
13 KiB
Python
Executable file
307 lines
13 KiB
Python
Executable file
import os
|
|
|
|
from PyQt6.QtWidgets import QLabel, QPushButton, QWidget
|
|
from PyQt6.QtGui import QPixmap
|
|
from PyQt6.QtCore import Qt, QTimer
|
|
|
|
from core.services.helpers.setup_sudo_scripts import auto_install_sudo_script, test_if_in_sudo_folder, is_singbox_wrapper_ready
|
|
from core.services.helpers.manage_assets import sudo_assets_folder_setup
|
|
from core.models.Result import Result, ResultError
|
|
|
|
import sys
|
|
|
|
from gui.v2.actions.singbox_prereqs import singbox_prereqs_installed
|
|
from gui.v2.ui.pages.Page import Page
|
|
from gui.v2.workers.worker_thread import WorkerThread
|
|
|
|
|
|
class NetworkingSetupPage(Page):
|
|
def __init__(self, page_stack, main_window=None, parent=None):
|
|
super().__init__("NetworkingSetup", page_stack, main_window, parent)
|
|
self.btn_path = main_window.btn_path
|
|
self.update_status = main_window
|
|
self.manual_scripts_ready = False
|
|
self.singbox_completion = None
|
|
self.singbox_setup_started = False
|
|
self.singbox_worker = None
|
|
self.latest_singbox_message = ""
|
|
self.setStyleSheet("font-family: Arial;")
|
|
self.button_next.clicked.disconnect(self.gestionar_next)
|
|
self.button_next.setVisible(True)
|
|
self.button_next.clicked.connect(self.go_to_install)
|
|
self._setup_ui()
|
|
|
|
def showEvent(self, event):
|
|
super().showEvent(event)
|
|
self.button_next.setVisible(True)
|
|
self.button_next.raise_()
|
|
if self._is_singbox_mode():
|
|
self.button_next.setVisible(False)
|
|
QTimer.singleShot(0, self.prepare_singbox_prereqs)
|
|
|
|
def _setup_ui(self):
|
|
header_icon = self._icon_label("networking_shield.png", 48, self)
|
|
header_icon.setGeometry(74, 66, 48, 48)
|
|
|
|
self.title_label = QLabel("Firewall & Managed-DNS Setup", self)
|
|
self.title_label.setGeometry(138, 68, 590, 44)
|
|
self.title_label.setStyleSheet("font-family: Arial; font-size: 26px; font-weight: bold; color: cyan;")
|
|
|
|
self.subtitle_label = QLabel(
|
|
"Optional privileged networking helpers",
|
|
self)
|
|
self.subtitle_label.setGeometry(140, 108, 560, 24)
|
|
self.subtitle_label.setStyleSheet("font-family: Arial; font-size: 14px; color: #d8ffff;")
|
|
|
|
self.description_label = QLabel(
|
|
"These firewall and managed-DNS helpers are separate bash scripts. "
|
|
"They stay outside the AppImage Python runtime, and if installed they are placed "
|
|
"in sudo-protected folders owned by root.",
|
|
self)
|
|
self.description_label.setGeometry(80, 152, 640, 72)
|
|
self.description_label.setWordWrap(True)
|
|
self.description_label.setStyleSheet("font-family: Arial; font-size: 15px; color: cyan;")
|
|
|
|
manual_panel = self._option_panel(74, 248, "Option 1", "Manual review")
|
|
manual_text = QLabel(
|
|
"Copy the scripts to Downloads, review them, then run sudo bash setup.sh yourself.",
|
|
manual_panel)
|
|
manual_text.setGeometry(24, 76, 252, 48)
|
|
manual_text.setWordWrap(True)
|
|
manual_text.setStyleSheet("font-family: Arial; font-size: 13px; color: #d8ffff;")
|
|
|
|
self.manual_button = QPushButton("Get scripts", manual_panel)
|
|
self.manual_button.setGeometry(24, 132, 252, 38)
|
|
self.manual_button.setStyleSheet(self._button_style("#007AFF", "#0056CC"))
|
|
self.manual_button.clicked.connect(self.handle_manual_setup)
|
|
|
|
auto_panel = self._option_panel(426, 248, "Option 2", "Automated installer")
|
|
auto_text = QLabel(
|
|
"Let HydraVeil run the installer, then test that the scripts reached the sudo folder.",
|
|
auto_panel)
|
|
auto_text.setGeometry(24, 76, 265, 48)
|
|
auto_text.setWordWrap(True)
|
|
auto_text.setStyleSheet("font-family: Arial; font-size: 13px; color: #d8ffff;")
|
|
|
|
self.auto_button = QPushButton("Automated installer", auto_panel)
|
|
self.auto_button.setGeometry(24, 132, 252, 38)
|
|
self.auto_button.setStyleSheet(self._button_style("#16a085", "#117a65"))
|
|
self.auto_button.clicked.connect(self.install_sudo_scripts_automatically)
|
|
|
|
self.status_label = QLabel("Choose manual or automated setup, or press Next to skip and continue.", self)
|
|
self.status_label.setGeometry(82, 462, 636, 38)
|
|
self.status_label.setWordWrap(True)
|
|
self.status_label.setStyleSheet("font-family: Arial; font-size: 13px; color: #d8ffff;")
|
|
|
|
|
|
def _option_panel(self, x, y, label, title):
|
|
panel = QWidget(self)
|
|
panel.setObjectName("networkingOptionPanel")
|
|
panel.setGeometry(x, y, 300, 186)
|
|
panel.setStyleSheet("""
|
|
QWidget#networkingOptionPanel {
|
|
background-color: #17323d;
|
|
border: 1px solid #2c7a8d;
|
|
border-radius: 6px;
|
|
}
|
|
""")
|
|
|
|
option_label = QLabel(label, panel)
|
|
option_label.setGeometry(24, 20, 120, 20)
|
|
option_label.setStyleSheet("font-family: Arial; font-size: 11px; font-weight: bold; color: #8de9ff; border: none;")
|
|
|
|
title_label = QLabel(title, panel)
|
|
title_label.setGeometry(24, 42, 240, 28)
|
|
title_label.setStyleSheet("font-family: Arial; font-size: 18px; font-weight: bold; color: cyan; border: none;")
|
|
|
|
return panel
|
|
|
|
def _icon_label(self, icon_name, size, parent):
|
|
label = QLabel(parent)
|
|
icon_path = os.path.join(self.btn_path, icon_name)
|
|
label.setPixmap(QPixmap(icon_path).scaled(
|
|
size, size, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation))
|
|
label.setFixedSize(size, size)
|
|
label.setStyleSheet("border: none;")
|
|
return label
|
|
|
|
def _button_style(self, background, hover):
|
|
return f"""
|
|
QPushButton {{
|
|
background: {background};
|
|
color: #f4ffff;
|
|
border: none;
|
|
border-radius: 4px;
|
|
font-family: Arial;
|
|
font-size: 12px;
|
|
font-weight: bold;
|
|
padding: 4px 8px;
|
|
}}
|
|
QPushButton:hover {{
|
|
background: {hover};
|
|
}}
|
|
QPushButton:disabled {{
|
|
background: #3f4954;
|
|
color: #94a3ad;
|
|
}}
|
|
"""
|
|
|
|
def _set_status(self, message, color="#d8ffff"):
|
|
self.status_label.setText(message)
|
|
self.status_label.setStyleSheet(f"font-family: Arial; font-size: 13px; color: {color};")
|
|
self.update_status.update_status(message)
|
|
|
|
def _result_is_valid(self, result: Result):
|
|
if hasattr(result, "valid"):
|
|
return result.valid
|
|
if result is None:
|
|
return True
|
|
return bool(result)
|
|
|
|
def _result_message(self, result: Result, fallback):
|
|
return getattr(result, "message", fallback)
|
|
|
|
def _is_singbox_mode(self):
|
|
return self.singbox_completion is not None
|
|
|
|
def configure_for_singbox_profile(self, completion_callback):
|
|
self.singbox_completion = completion_callback
|
|
self.singbox_setup_started = False
|
|
self.singbox_worker = None
|
|
self.latest_singbox_message = ""
|
|
self.manual_scripts_ready = False
|
|
self.title_label.setText("Singbox Prerequisites")
|
|
self.subtitle_label.setText("Required for Hysteria2 and VLESS systemwide profiles")
|
|
self.description_label.setText(
|
|
"Hysteria2 and VLESS need the firewall, managed-DNS, Singbox wrapper, "
|
|
"and Singbox binary before the profile can be created.")
|
|
self.manual_button.setText("Get scripts")
|
|
self.manual_button.setDisabled(False)
|
|
self.auto_button.setDisabled(False)
|
|
self.button_next.setVisible(False)
|
|
self._set_status("Checking Singbox prerequisites...")
|
|
return self
|
|
|
|
def _sudo_scripts_ready_for_singbox(self):
|
|
if not is_singbox_wrapper_ready():
|
|
return False
|
|
confirmed = test_if_in_sudo_folder()
|
|
return confirmed.valid
|
|
|
|
def prepare_singbox_prereqs(self):
|
|
if not self._is_singbox_mode():
|
|
return
|
|
if singbox_prereqs_installed():
|
|
self._set_status("Singbox prerequisites are already installed. Creating profile...", "#2ecc71")
|
|
QTimer.singleShot(200, self.finish_singbox_prereq_flow)
|
|
return
|
|
if not self._sudo_scripts_ready_for_singbox():
|
|
self.manual_button.setDisabled(False)
|
|
self.auto_button.setDisabled(False)
|
|
self.button_next.setVisible(False)
|
|
self._set_status("Firewall, managed-DNS, and the Singbox wrapper are required. Choose manual or automated setup.", "#d8ffff")
|
|
return
|
|
self.start_singbox_binary_setup()
|
|
|
|
def start_singbox_binary_setup(self):
|
|
if self.singbox_setup_started:
|
|
return
|
|
self.singbox_setup_started = True
|
|
self.latest_singbox_message = ""
|
|
self.manual_button.setDisabled(True)
|
|
self.auto_button.setDisabled(True)
|
|
self.button_next.setVisible(False)
|
|
self._set_status("Sudo wrapper scripts are ready. Setting up Singbox binary...")
|
|
self.singbox_worker = WorkerThread('SETUP_SINGBOX_BINARY')
|
|
self.singbox_worker.text_output.connect(self._set_singbox_status)
|
|
self.singbox_worker.finished.connect(self.on_singbox_setup_finished)
|
|
self.singbox_worker.start()
|
|
|
|
def _set_singbox_status(self, message):
|
|
self.latest_singbox_message = message
|
|
self._set_status(message)
|
|
|
|
def on_singbox_setup_finished(self, success):
|
|
if success:
|
|
self._set_status("Singbox setup complete. Creating profile...", "#2ecc71")
|
|
QTimer.singleShot(600, self.finish_singbox_prereq_flow)
|
|
return
|
|
self.singbox_setup_started = False
|
|
self.manual_button.setDisabled(False)
|
|
self.auto_button.setDisabled(False)
|
|
self.button_next.setVisible(True)
|
|
self._set_status(self.latest_singbox_message or "Singbox setup failed.", "#ff6b6b")
|
|
|
|
def finish_singbox_prereq_flow(self):
|
|
completion_callback = self.singbox_completion
|
|
self.singbox_completion = None
|
|
self.singbox_setup_started = False
|
|
self.latest_singbox_message = ""
|
|
self.title_label.setText("Firewall & Managed-DNS Setup")
|
|
self.subtitle_label.setText("Optional privileged networking helpers")
|
|
self.description_label.setText(
|
|
"These firewall and managed-DNS helpers are separate bash scripts. "
|
|
"They stay outside the AppImage Python runtime, and if installed they are placed "
|
|
"in sudo-protected folders owned by root.")
|
|
self.manual_button.setText("Get scripts")
|
|
self.manual_button.setDisabled(False)
|
|
self.auto_button.setDisabled(False)
|
|
if completion_callback is not None:
|
|
completion_callback()
|
|
|
|
def handle_manual_setup(self):
|
|
if self.manual_scripts_ready:
|
|
self.confirm_sudo_scripts()
|
|
return
|
|
results = sudo_assets_folder_setup()
|
|
if not self._result_is_valid(results):
|
|
self._set_status(
|
|
f"Could not copy scripts: {self._result_message(results, 'Unknown error')}", "#ff6b6b")
|
|
return
|
|
self.manual_scripts_ready = True
|
|
self.manual_button.setText("Confirm manual install")
|
|
self._set_status(
|
|
"Copied to Downloads/hydraveil_sudo_scripts. Review them, run 'sudo bash setup.sh', then click Confirm manual install.", "#d8ffff")
|
|
|
|
def confirm_sudo_scripts(self):
|
|
confirmed = test_if_in_sudo_folder()
|
|
if confirmed.valid:
|
|
if self._is_singbox_mode():
|
|
self._set_status("Confirmed it worked. Continuing to Singbox setup.", "#2ecc71")
|
|
QTimer.singleShot(600, self.start_singbox_binary_setup)
|
|
return
|
|
self._set_status("Confirmed it worked. Continuing to prerequisite installation.", "#2ecc71")
|
|
QTimer.singleShot(600, self.go_to_install)
|
|
else:
|
|
self._set_status(
|
|
f"I'm sorry it did not work, {confirmed.message}", "#ff6b6b")
|
|
|
|
def install_sudo_scripts_automatically(self):
|
|
results = auto_install_sudo_script()
|
|
if results.valid:
|
|
confirmed = test_if_in_sudo_folder()
|
|
if confirmed.valid:
|
|
if self._is_singbox_mode():
|
|
self._set_status("The setup script ran successfully. Continuing to Singbox setup.", "#2ecc71")
|
|
QTimer.singleShot(600, self.start_singbox_binary_setup)
|
|
return
|
|
self._set_status("The setup script ran successfully. Continuing to prerequisite installation.", "#2ecc71")
|
|
QTimer.singleShot(600, self.go_to_install)
|
|
else:
|
|
self._set_status(
|
|
f"I'm sorry it did not work, {confirmed.message}", "#ff6b6b")
|
|
else:
|
|
self._set_status(
|
|
f"Setup script did NOT work because {results.message}", "#ff6b6b")
|
|
|
|
def go_to_install(self):
|
|
if self._is_singbox_mode():
|
|
self.prepare_singbox_prereqs()
|
|
return
|
|
self.custom_window.navigator.navigate("install_system_package")
|
|
install_page = self.custom_window.navigator.get_cached("install_system_package")
|
|
if install_page is not None:
|
|
install_page.configure(package_name='all', distro='debian')
|
|
|
|
def back_to_welcome(self):
|
|
self.custom_window.navigator.navigate("welcome")
|