large update
11
Changelog
@ -7,23 +7,26 @@ My standard System: Linux Mint 22 Cinnamon
|
||||
- If Wire-Py already runs, prevent further start
|
||||
- for loops with lists replaced by List Comprehensions
|
||||
|
||||
|
||||
### Added
|
||||
13-04-0725
|
||||
03-06-2025
|
||||
|
||||
-
|
||||
### Added
|
||||
13-04-20255
|
||||
|
||||
- Installer update for Open Suse Tumbleweed and Leap
|
||||
- add symbolic link wirepy.py
|
||||
|
||||
|
||||
### Added
|
||||
09-04-0725
|
||||
09-04-2025
|
||||
|
||||
- Installer now with query and remove
|
||||
- Icons merged
|
||||
|
||||
|
||||
### Added
|
||||
07-04-0725
|
||||
07-04-2025
|
||||
|
||||
- Installers will support other systems again
|
||||
- Installer is now finished clean with wrong password
|
||||
|
883
common_tools.py
@ -1,883 +0,0 @@
|
||||
""" Classes Method and Functions for lx Apps """
|
||||
|
||||
import getpass
|
||||
import shutil
|
||||
import signal
|
||||
import base64
|
||||
import secrets
|
||||
import subprocess
|
||||
from subprocess import CompletedProcess, run
|
||||
import re
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from typing import Optional, Dict, Any, NoReturn
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from tkinter import ttk, Toplevel
|
||||
from wp_app_config import AppConfig, Msg, logging
|
||||
import requests
|
||||
|
||||
# Translate
|
||||
_ = AppConfig.setup_translations()
|
||||
|
||||
|
||||
class CryptoUtil:
|
||||
"""
|
||||
This class is for the creation of the folders and files
|
||||
required by Wire-Py, as well as for decryption
|
||||
the tunnel from the user's home directory
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def decrypt(user) -> None:
|
||||
"""
|
||||
Starts SSL dencrypt
|
||||
"""
|
||||
if len([file.stem for file in AppConfig.CONFIG_DIR.glob("*.dat")]) == 0:
|
||||
pass
|
||||
else:
|
||||
process: CompletedProcess[str] = run(
|
||||
["pkexec", "/usr/local/bin/ssl_decrypt.py", "--user", user],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
# Output from Openssl Error
|
||||
if process.stderr:
|
||||
logging.error(process.stderr, exc_info=True)
|
||||
|
||||
if process.returncode == 0:
|
||||
logging.info("Files successfully decrypted...", exc_info=True)
|
||||
else:
|
||||
logging.error(
|
||||
f"Error process decrypt: Code {process.returncode}", exc_info=True
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def encrypt(user) -> None:
|
||||
"""
|
||||
Starts SSL encryption
|
||||
"""
|
||||
process: CompletedProcess[str] = run(
|
||||
["pkexec", "/usr/local/bin/ssl_encrypt.py", "--user", user],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
# Output from Openssl Error
|
||||
if process.stderr:
|
||||
logging.error(process.stderr, exc_info=True)
|
||||
|
||||
if process.returncode == 0:
|
||||
logging.info("Files successfully encrypted...", exc_info=True)
|
||||
else:
|
||||
logging.error(
|
||||
f"Error process encrypt: Code {process.returncode}", exc_info=True
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def find_key(key: str = "") -> bool:
|
||||
"""
|
||||
Checks if the private key already exists in the system using an external script.
|
||||
Returns True only if the full key is found exactly (no partial match).
|
||||
"""
|
||||
process: CompletedProcess[str] = run(
|
||||
["pkexec", "/usr/local/bin/match_found.py", key],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if "True" in process.stdout:
|
||||
return True
|
||||
elif "False" in process.stdout:
|
||||
return False
|
||||
logging.error(
|
||||
f"Unexpected output from the external script:\nSTDOUT: {process.stdout}\nSTDERR: {process.stderr}",
|
||||
exc_info=True,
|
||||
)
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_valid_base64(key: str) -> bool:
|
||||
"""
|
||||
Validates if the input is a valid Base64 string (WireGuard private key format).
|
||||
Returns True only for non-empty strings that match the expected length.
|
||||
"""
|
||||
# Check for empty string
|
||||
if not key or key.strip() == "":
|
||||
return False
|
||||
|
||||
# Regex pattern to validate Base64: [A-Za-z0-9+/]+={0,2}
|
||||
base64_pattern = r"^[A-Za-z0-9+/]+={0,2}$"
|
||||
if not re.match(base64_pattern, key):
|
||||
return False
|
||||
|
||||
try:
|
||||
# Decode and check length (WireGuard private keys are 32 bytes long)
|
||||
decoded = base64.b64decode(key)
|
||||
if len(decoded) != 32: # 32 bytes = 256 bits
|
||||
return False
|
||||
except Exception as e:
|
||||
logging.error(f"Error on decode Base64: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class LxTools:
|
||||
"""
|
||||
Class LinuxTools methods that can also be used for other apps
|
||||
"""
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def center_window_cross_platform(window, width, height):
|
||||
"""
|
||||
Centers a window on the primary monitor in a way that works on both X11 and Wayland
|
||||
|
||||
Args:
|
||||
window: The tkinter window to center
|
||||
width: Window width
|
||||
height: Window height
|
||||
"""
|
||||
# Calculate the position before showing the window
|
||||
|
||||
# First attempt: Try to use GDK if available (works on both X11 and Wayland)
|
||||
try:
|
||||
import gi
|
||||
|
||||
gi.require_version("Gdk", "3.0")
|
||||
from gi.repository import Gdk
|
||||
|
||||
display = Gdk.Display.get_default()
|
||||
monitor = display.get_primary_monitor() or display.get_monitor(0)
|
||||
geometry = monitor.get_geometry()
|
||||
scale_factor = monitor.get_scale_factor()
|
||||
|
||||
# Calculate center position on the primary monitor
|
||||
x = geometry.x + (geometry.width - width // scale_factor) // 2
|
||||
y = geometry.y + (geometry.height - height // scale_factor) // 2
|
||||
|
||||
# Set window geometry
|
||||
window.geometry(f"{width}x{height}+{x}+{y}")
|
||||
return
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
|
||||
# Second attempt: Try xrandr for X11
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
output = subprocess.check_output(
|
||||
["xrandr", "--query"], universal_newlines=True
|
||||
)
|
||||
|
||||
# Parse the output to find the primary monitor
|
||||
primary_info = None
|
||||
for line in output.splitlines():
|
||||
if "primary" in line:
|
||||
parts = line.split()
|
||||
for part in parts:
|
||||
if "x" in part and "+" in part:
|
||||
primary_info = part
|
||||
break
|
||||
break
|
||||
|
||||
if primary_info:
|
||||
# Parse the geometry: WIDTH x HEIGHT+X+Y
|
||||
geometry = primary_info.split("+")
|
||||
dimensions = geometry[0].split("x")
|
||||
primary_width = int(dimensions[0])
|
||||
primary_height = int(dimensions[1])
|
||||
primary_x = int(geometry[1])
|
||||
primary_y = int(geometry[2])
|
||||
|
||||
# Calculate center position on the primary monitor
|
||||
x = primary_x + (primary_width - width) // 2
|
||||
y = primary_y + (primary_height - height) // 2
|
||||
|
||||
# Set window geometry
|
||||
window.geometry(f"{width}x{height}+{x}+{y}")
|
||||
return
|
||||
except (ImportError, IndexError, ValueError):
|
||||
pass
|
||||
|
||||
# Final fallback: Use standard Tkinter method
|
||||
screen_width = window.winfo_screenwidth()
|
||||
screen_height = window.winfo_screenheight()
|
||||
|
||||
# Try to make an educated guess for multi-monitor setups
|
||||
# If screen width is much larger than height, assume multiple monitors side by side
|
||||
if (
|
||||
screen_width > screen_height * 1.8
|
||||
): # Heuristic for detecting multiple monitors
|
||||
# Assume the primary monitor is on the left half
|
||||
screen_width = screen_width // 2
|
||||
|
||||
x = (screen_width - width) // 2
|
||||
y = (screen_height - height) // 2
|
||||
window.geometry(f"{width}x{height}+{x}+{y}")
|
||||
|
||||
@staticmethod
|
||||
def clean_files(tmp_dir: Path = AppConfig.TEMP_DIR, file: Path = None) -> None:
|
||||
"""
|
||||
Deletes temporary files and directories for cleanup when exiting the application.
|
||||
|
||||
This method safely removes an optional directory defined by `AppConfig.TEMP_DIR`
|
||||
and a single file to free up resources at the end of the program's execution.
|
||||
All operations are performed securely, and errors such as `FileNotFoundError`
|
||||
are ignored if the target files or directories do not exist.
|
||||
:param tmp_dir: (Path, optional): Path to the temporary directory that should be deleted.
|
||||
If `None`, the value of `AppConfig.TEMP_DIR` is used.
|
||||
:param file: (Path, optional): Path to the file that should be deleted.
|
||||
If `None`, no additional file will be deleted.
|
||||
|
||||
Returns:
|
||||
None: The method does not return any value.
|
||||
"""
|
||||
|
||||
if AppConfig.TEMP_DIR is not None:
|
||||
shutil.rmtree(AppConfig.TEMP_DIR)
|
||||
try:
|
||||
if file is not None:
|
||||
Path.unlink(file)
|
||||
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def msg_window(
|
||||
image_path: Path,
|
||||
image_path2: Path,
|
||||
w_title: str,
|
||||
w_txt: str,
|
||||
txt2: Optional[str] = None,
|
||||
com: Optional[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Creates message windows
|
||||
|
||||
:param image_path2:
|
||||
:param image_path:
|
||||
AppConfig.IMAGE_PATHS["icon_info"] = Image for TK window which is displayed to the left of the text
|
||||
AppConfig.IMAGE_PATHS["icon_vpn"] = Image for Task Icon
|
||||
:argument w_title = Windows Title
|
||||
:argument w_txt = Text for Tk Window
|
||||
:argument txt2 = Text for Button two
|
||||
:argument com = function for Button command
|
||||
"""
|
||||
msg: tk.Toplevel = tk.Toplevel()
|
||||
msg.resizable(width=False, height=False)
|
||||
msg.title(w_title)
|
||||
msg.configure(pady=15, padx=15)
|
||||
|
||||
# load first image for a window
|
||||
try:
|
||||
msg.img = tk.PhotoImage(file=image_path)
|
||||
msg.i_window = tk.Label(msg, image=msg.img)
|
||||
except Exception as e:
|
||||
logging.error(f"Error on load Window Image: {e}", exc_info=True)
|
||||
msg.i_window = tk.Label(msg, text="Image not found")
|
||||
|
||||
label: tk.Label = tk.Label(msg, text=w_txt)
|
||||
label.grid(column=1, row=0)
|
||||
|
||||
if txt2 is not None and com is not None:
|
||||
label.config(font=("Ubuntu", 11), padx=15, justify="left")
|
||||
msg.i_window.grid(column=0, row=0, sticky="nw")
|
||||
button2: ttk.Button = ttk.Button(
|
||||
msg, text=f"{txt2}", command=com, padding=4
|
||||
)
|
||||
button2.grid(column=0, row=1, sticky="e", columnspan=2)
|
||||
button: ttk.Button = ttk.Button(
|
||||
msg, text="OK", command=msg.destroy, padding=4
|
||||
)
|
||||
button.grid(column=0, row=1, sticky="w", columnspan=2)
|
||||
else:
|
||||
label.config(font=("Ubuntu", 11), padx=15)
|
||||
msg.i_window.grid(column=0, row=0)
|
||||
button: ttk.Button = ttk.Button(
|
||||
msg, text="OK", command=msg.destroy, padding=4
|
||||
)
|
||||
button.grid(column=0, columnspan=2, row=1)
|
||||
|
||||
try:
|
||||
icon = tk.PhotoImage(file=image_path2)
|
||||
msg.iconphoto(True, icon)
|
||||
except Exception as e:
|
||||
logging.error(f"Error loading the window icon: {e}", exc_info=True)
|
||||
|
||||
msg.columnconfigure(0, weight=1)
|
||||
msg.rowconfigure(0, weight=1)
|
||||
msg.winfo_toplevel()
|
||||
|
||||
@staticmethod
|
||||
def sigi(file_path: Optional[Path] = None, file: Optional[Path] = None) -> None:
|
||||
"""
|
||||
Function for cleanup after a program interruption
|
||||
|
||||
:param file: Optional - File to be deleted
|
||||
:param file_path: Optional - Directory to be deleted
|
||||
"""
|
||||
|
||||
def signal_handler(signum: int, frame: Any) -> NoReturn:
|
||||
"""
|
||||
Determines clear text names for signal numbers and handles signals
|
||||
|
||||
Args:
|
||||
signum: The signal number
|
||||
frame: The current stack frame
|
||||
|
||||
Returns:
|
||||
NoReturn since the function either exits the program or continues execution
|
||||
"""
|
||||
|
||||
signals_to_names_dict: Dict[int, str] = dict(
|
||||
(getattr(signal, n), n)
|
||||
for n in dir(signal)
|
||||
if n.startswith("SIG") and "_" not in n
|
||||
)
|
||||
|
||||
signal_name: str = signals_to_names_dict.get(
|
||||
signum, f"Unnamed signal: {signum}"
|
||||
)
|
||||
|
||||
# End program for certain signals, report to others only reception
|
||||
if signum in (signal.SIGINT, signal.SIGTERM):
|
||||
exit_code: int = 1
|
||||
logging.error(
|
||||
f"\nSignal {signal_name} {signum} received. => Aborting with exit code {exit_code}.",
|
||||
exc_info=True,
|
||||
)
|
||||
LxTools.clean_files(file_path, file)
|
||||
logging.info("Breakdown by user...")
|
||||
sys.exit(exit_code)
|
||||
else:
|
||||
logging.info(f"Signal {signum} received and ignored.")
|
||||
LxTools.clean_files(file_path, file)
|
||||
logging.error("Process unexpectedly ended...")
|
||||
|
||||
# Register signal handlers for various signals
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
signal.signal(signal.SIGHUP, signal_handler)
|
||||
|
||||
|
||||
class Tunnel:
|
||||
"""
|
||||
Class of Methods for Wire-Py
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def parse_files_to_dictionary(
|
||||
directory: Path = None, filepath: str = None, content: str = None
|
||||
) -> tuple[dict, str] | dict | None:
|
||||
data = {}
|
||||
|
||||
if filepath is not None:
|
||||
filepath = Path(filepath)
|
||||
try:
|
||||
content = filepath.read_text()
|
||||
|
||||
# parse the content
|
||||
address_line = next(
|
||||
line for line in content.splitlines() if line.startswith("Address")
|
||||
)
|
||||
dns_line = next(
|
||||
line for line in content.splitlines() if line.startswith("DNS")
|
||||
)
|
||||
endpoint_line = next(
|
||||
line for line in content.splitlines() if line.startswith("Endpoint")
|
||||
)
|
||||
private_key_line = next(
|
||||
line
|
||||
for line in content.splitlines()
|
||||
if line.startswith("PrivateKey")
|
||||
)
|
||||
|
||||
content = secrets.token_bytes(len(content))
|
||||
|
||||
# extract the values
|
||||
address = address_line.split("=")[1].strip()
|
||||
dns = dns_line.split("=")[1].strip()
|
||||
endpoint = endpoint_line.split("=")[1].strip()
|
||||
private_key = private_key_line.split("=")[1].strip()
|
||||
|
||||
# Shorten the tunnel name to the maximum allowed length if it exceeds 12 characters.
|
||||
original_stem = filepath.stem
|
||||
truncated_stem = (
|
||||
original_stem[-12:] if len(original_stem) > 12 else original_stem
|
||||
)
|
||||
|
||||
# save in the dictionary
|
||||
data[truncated_stem] = {
|
||||
"Address": address,
|
||||
"DNS": dns,
|
||||
"Endpoint": endpoint,
|
||||
"PrivateKey": private_key,
|
||||
}
|
||||
|
||||
content = secrets.token_bytes(len(content))
|
||||
|
||||
except StopIteration:
|
||||
pass
|
||||
|
||||
elif directory is not None:
|
||||
|
||||
if not directory.exists() or not directory.is_dir():
|
||||
logging.error(
|
||||
"Temp directory does not exist or is not a directory.",
|
||||
exc_info=True,
|
||||
)
|
||||
return None
|
||||
|
||||
# Get a list of all files in the directory
|
||||
files = [file for file in AppConfig.TEMP_DIR.iterdir() if file.is_file()]
|
||||
|
||||
# Search for the string in the files
|
||||
for file in files:
|
||||
try:
|
||||
content = file.read_text()
|
||||
# parse the content
|
||||
address_line = next(
|
||||
line
|
||||
for line in content.splitlines()
|
||||
if line.startswith("Address")
|
||||
)
|
||||
dns_line = next(
|
||||
line for line in content.splitlines() if line.startswith("DNS")
|
||||
)
|
||||
endpoint_line = next(
|
||||
line
|
||||
for line in content.splitlines()
|
||||
if line.startswith("Endpoint")
|
||||
)
|
||||
|
||||
# extract values
|
||||
address = address_line.split("=")[1].strip()
|
||||
dns = dns_line.split("=")[1].strip()
|
||||
endpoint = endpoint_line.split("=")[1].strip()
|
||||
|
||||
# save values to dictionary
|
||||
data[file.stem] = {
|
||||
"Address": address,
|
||||
"DNS": dns,
|
||||
"Endpoint": endpoint,
|
||||
}
|
||||
|
||||
except Exception:
|
||||
# Ignore errors and continue to the next file
|
||||
continue
|
||||
if content is not None:
|
||||
content = secrets.token_bytes(len(content))
|
||||
if filepath is not None:
|
||||
return data, truncated_stem
|
||||
else:
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def get_active() -> str:
|
||||
"""
|
||||
Shows the Active Tunnel
|
||||
"""
|
||||
active = None
|
||||
try:
|
||||
process: CompletedProcess[str] = run(
|
||||
["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show", "--active"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
active = next(
|
||||
line.split(":")[0].strip()
|
||||
for line in process.stdout.splitlines()
|
||||
if line.endswith("wireguard")
|
||||
)
|
||||
|
||||
if process.stderr and "error" in process.stderr.lower():
|
||||
logging.error(f"Error output on nmcli: {process.stderr}")
|
||||
|
||||
except StopIteration:
|
||||
active = None
|
||||
except Exception as e:
|
||||
logging.error(f"Error on nmcli: {e}")
|
||||
active = None
|
||||
|
||||
return active if active is not None else ""
|
||||
|
||||
@staticmethod
|
||||
def export() -> bool | None:
|
||||
"""
|
||||
This will export the tunnels.
|
||||
A zipfile with the current date and time is created
|
||||
in the user's home directory with the correct right
|
||||
"""
|
||||
now_time: datetime = datetime.now()
|
||||
now_datetime: str = now_time.strftime("wg-exp-%m-%d-%Y-%H:%M")
|
||||
|
||||
try:
|
||||
AppConfig.ensure_directories()
|
||||
CryptoUtil.decrypt(getpass.getuser())
|
||||
if len([file.name for file in AppConfig.TEMP_DIR.glob("*.conf")]) == 0:
|
||||
|
||||
LxTools.msg_window(
|
||||
AppConfig.IMAGE_PATHS["icon_info"],
|
||||
AppConfig.IMAGE_PATHS["icon_msg"],
|
||||
Msg.STR["sel_tl"],
|
||||
Msg.STR["tl_first"],
|
||||
)
|
||||
return False
|
||||
else:
|
||||
wg_tar: str = f"{AppConfig.BASE_DIR}/{now_datetime}"
|
||||
try:
|
||||
shutil.make_archive(wg_tar, "zip", AppConfig.TEMP_DIR)
|
||||
with zipfile.ZipFile(f"{wg_tar}.zip", "r") as zf:
|
||||
if zf.namelist():
|
||||
|
||||
LxTools.msg_window(
|
||||
AppConfig.IMAGE_PATHS["icon_info"],
|
||||
AppConfig.IMAGE_PATHS["icon_vpn"],
|
||||
Msg.STR["exp_succ"],
|
||||
Msg.STR["exp_in_home"],
|
||||
)
|
||||
else:
|
||||
logging.error(
|
||||
"There was a mistake at creating the Zip file. File is empty."
|
||||
)
|
||||
LxTools.msg_window(
|
||||
AppConfig.IMAGE_PATHS["icon_error"],
|
||||
AppConfig.IMAGE_PATHS["icon_msg"],
|
||||
Msg.STR["exp_err"],
|
||||
Msg.STR["exp_zip"],
|
||||
)
|
||||
return False
|
||||
return True
|
||||
except PermissionError:
|
||||
logging.error(
|
||||
f"Permission denied when creating archive in {wg_tar}"
|
||||
)
|
||||
return False
|
||||
|
||||
except zipfile.BadZipFile as e:
|
||||
logging.error(f"Invalid ZIP file: {e}")
|
||||
return False
|
||||
except TypeError:
|
||||
pass
|
||||
except Exception as e:
|
||||
logging.error(f"Export failed: {str(e)}")
|
||||
LxTools.msg_window(
|
||||
AppConfig.IMAGE_PATHS["icon_error"],
|
||||
AppConfig.IMAGE_PATHS["icon_msg"],
|
||||
Msg.STR["exp_err"],
|
||||
Msg.STR["exp_try"],
|
||||
)
|
||||
return False
|
||||
|
||||
finally:
|
||||
LxTools.clean_files(AppConfig.TEMP_DIR)
|
||||
AppConfig.ensure_directories()
|
||||
|
||||
|
||||
# ConfigManager with caching
|
||||
class ConfigManager:
|
||||
"""
|
||||
Universal class for managing configuration files with caching support.
|
||||
|
||||
This class provides a general solution to load, save, and manage configuration
|
||||
files across different projects. It uses a caching system to optimize access efficiency.
|
||||
The `init()` method initializes the configuration file path, while `load()` and `save()`
|
||||
synchronize data between the file and internal memory structures.
|
||||
|
||||
Key Features:
|
||||
- Caching to minimize I/O operations.
|
||||
- Default values for missing or corrupted configuration files.
|
||||
- Reusability across different projects and use cases.
|
||||
|
||||
The class is designed for central application configuration management, working closely
|
||||
with `ThemeManager` to dynamically manage themes or other settings.
|
||||
"""
|
||||
|
||||
_config = None
|
||||
_config_file = None
|
||||
|
||||
@classmethod
|
||||
def init(cls, config_file):
|
||||
"""Initial the Configmanager with the given config file"""
|
||||
cls._config_file = config_file
|
||||
cls._config = None # Reset the cache
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
"""Load the config file and return the config as dict"""
|
||||
if not cls._config:
|
||||
try:
|
||||
lines = Path(cls._config_file).read_text(encoding="utf-8").splitlines()
|
||||
cls._config = {
|
||||
"updates": lines[1].strip(),
|
||||
"theme": lines[3].strip(),
|
||||
"tooltips": lines[5].strip()
|
||||
== "True", # is converted here to boolean!!!
|
||||
"autostart": lines[7].strip() if len(lines) > 7 else "off",
|
||||
}
|
||||
except (IndexError, FileNotFoundError):
|
||||
# DeDefault values in case of error
|
||||
cls._config = {
|
||||
"updates": "on",
|
||||
"theme": "light",
|
||||
"tooltips": "True", # Default Value as string!
|
||||
"autostart": "off",
|
||||
}
|
||||
return cls._config
|
||||
|
||||
@classmethod
|
||||
def save(cls):
|
||||
"""Save the config to the config file"""
|
||||
if cls._config:
|
||||
lines = [
|
||||
"# Configuration\n",
|
||||
f"{cls._config['updates']}\n",
|
||||
"# Theme\n",
|
||||
f"{cls._config['theme']}\n",
|
||||
"# Tooltips\n",
|
||||
f"{str(cls._config['tooltips'])}\n",
|
||||
"# Autostart\n",
|
||||
f"{cls._config['autostart']}\n",
|
||||
]
|
||||
Path(cls._config_file).write_text("".join(lines), encoding="utf-8")
|
||||
|
||||
@classmethod
|
||||
def set(cls, key, value):
|
||||
"""Sets a configuration value and saves the change"""
|
||||
cls.load()
|
||||
cls._config[key] = value
|
||||
cls.save()
|
||||
|
||||
@classmethod
|
||||
def get(cls, key, default=None):
|
||||
"""Returns a configuration value"""
|
||||
config = cls.load()
|
||||
return config.get(key, default)
|
||||
|
||||
|
||||
class ThemeManager:
|
||||
"""
|
||||
Class for central theme management and UI customization.
|
||||
|
||||
This static class allows dynamic adjustment of the application's appearance.
|
||||
The method `change_theme()` updates the current theme and saves
|
||||
the selection in the configuration file via `ConfigManager`.
|
||||
It ensures a consistent visual design across the entire project.
|
||||
|
||||
Key Features:
|
||||
- Central control over themes.
|
||||
- Automatic saving of theme settings to the configuration file.
|
||||
- Tight integration with `ConfigManager` for persistent storage of preferences.
|
||||
|
||||
The class is designed to apply themes consistently throughout the application,
|
||||
ensuring that changes are traceable and uniform across all parts of the project.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def change_theme(root, theme_in_use, theme_name=None):
|
||||
"""Change application theme centrally"""
|
||||
root.tk.call("set_theme", theme_in_use)
|
||||
if theme_in_use == theme_name:
|
||||
ConfigManager.set("theme", theme_in_use)
|
||||
|
||||
|
||||
class GiteaUpdate:
|
||||
"""
|
||||
Calling download requests the download URL of the running script,
|
||||
the taskbar image for the “Download OK” window, the taskbar image for the
|
||||
“Download error” window, and the variable res
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def api_down(update_api_url: str, version: str, update_setting: str = None) -> str:
|
||||
"""
|
||||
Checks for updates via API
|
||||
|
||||
Args:
|
||||
update_api_url: Update API URL
|
||||
version: Current version
|
||||
update_setting: Update setting from ConfigManager (on/off)
|
||||
|
||||
Returns:
|
||||
New version or status message
|
||||
"""
|
||||
# If updates are disabled, return immediately
|
||||
if update_setting != "on":
|
||||
return "False"
|
||||
|
||||
try:
|
||||
response: requests.Response = requests.get(update_api_url, timeout=10)
|
||||
response.raise_for_status() # Raise exception for HTTP errors
|
||||
|
||||
response_data = response.json()
|
||||
if not response_data:
|
||||
return "No Updates"
|
||||
|
||||
latest_version = response_data[0].get("tag_name")
|
||||
if not latest_version:
|
||||
return "Invalid API Response"
|
||||
|
||||
# Compare versions (strip 'v. ' prefix if present)
|
||||
current_version = version[3:] if version.startswith("v. ") else version
|
||||
|
||||
if current_version != latest_version:
|
||||
return latest_version
|
||||
else:
|
||||
return "No Updates"
|
||||
|
||||
except requests.exceptions.RequestException:
|
||||
return "No Internet Connection!"
|
||||
except (ValueError, KeyError, IndexError):
|
||||
return "Invalid API Response"
|
||||
|
||||
@staticmethod
|
||||
def download(
|
||||
urld: str,
|
||||
res: str,
|
||||
image_path: Path = None,
|
||||
image_path2: Path = None,
|
||||
image_path3: Path = None,
|
||||
image_path4: Path = None,
|
||||
) -> None:
|
||||
"""
|
||||
Downloads new version of wirepy
|
||||
|
||||
:param urld: Download URL
|
||||
:param res: Result filename
|
||||
:param image_path: AppConfig.IMAGE_PATHS["icon_info"]: Image for TK window which is displayed to the left of the text
|
||||
:param image_path2: AppConfig.IMAGE_PATHS["icon_vpn"]: Image for Task Icon
|
||||
:param image_path3: AppConfig.IMAGE_PATHS["icon_error"]: Image for TK window which is displayed to the left of the text
|
||||
:param image_path4: AppConfig.IMAGE_PATHS["icon_msg"]: Image for Task Icon
|
||||
"""
|
||||
try:
|
||||
to_down: str = f"wget -qP {Path.home()} {" "} {urld}"
|
||||
result: int = subprocess.call(to_down, shell=True)
|
||||
if result == 0:
|
||||
shutil.chown(f"{Path.home()}/{res}.zip", 1000, 1000)
|
||||
|
||||
wt: str = _("Download Successful")
|
||||
msg_t: str = _("Your zip file is in home directory")
|
||||
LxTools.msg_window(
|
||||
AppConfig.IMAGE_PATHS["icon_info"],
|
||||
AppConfig.IMAGE_PATHS["icon_vpn"],
|
||||
wt,
|
||||
msg_t,
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
wt: str = _("Download error")
|
||||
msg_t: str = _("Download failed! Please try again")
|
||||
LxTools.msg_window(
|
||||
AppConfig.IMAGE_PATHS["icon_error"],
|
||||
AppConfig.IMAGE_PATHS["icon_msg"],
|
||||
wt,
|
||||
msg_t,
|
||||
)
|
||||
|
||||
except subprocess.CalledProcessError:
|
||||
|
||||
wt: str = _("Download error")
|
||||
msg_t: str = _("Download failed! No internet connection!")
|
||||
LxTools.msg_window(
|
||||
AppConfig.IMAGE_PATHS["icon_error"],
|
||||
AppConfig.IMAGE_PATHS["icon_msg"],
|
||||
wt,
|
||||
msg_t,
|
||||
)
|
||||
|
||||
|
||||
class Tooltip:
|
||||
"""Class for Tooltip
|
||||
from common_tools.py import Tooltip
|
||||
example: Tooltip(label, "Show tooltip on label")
|
||||
example: Tooltip(button, "Show tooltip on button")
|
||||
example: Tooltip(widget, "Text", state_var=tk.BooleanVar())
|
||||
example: Tooltip(widget, "Text", state_var=tk.BooleanVar(), x_offset=20, y_offset=10)
|
||||
|
||||
info: label and button are parent widgets.
|
||||
NOTE: When using with state_var, pass the tk.BooleanVar object directly,
|
||||
NOT its value. For example: use state_var=my_bool_var, NOT state_var=my_bool_var.get()
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
widget: Any,
|
||||
text: str,
|
||||
state_var: Optional[tk.BooleanVar] = None,
|
||||
x_offset: int = 65,
|
||||
y_offset: int = 40,
|
||||
) -> None:
|
||||
"""Tooltip Class"""
|
||||
self.widget: Any = widget
|
||||
self.text: str = text
|
||||
self.tooltip_window: Optional[Toplevel] = None
|
||||
self.state_var = state_var
|
||||
self.x_offset = x_offset
|
||||
self.y_offset = y_offset
|
||||
|
||||
# Initial binding based on the current state
|
||||
self.update_bindings()
|
||||
|
||||
# Add trace to the state_var if provided
|
||||
if self.state_var is not None:
|
||||
self.state_var.trace_add("write", self.update_bindings)
|
||||
|
||||
def update_bindings(self, *args) -> None:
|
||||
"""Updates the bindings based on the current state"""
|
||||
# Remove existing bindings first
|
||||
self.widget.unbind("<Enter>")
|
||||
self.widget.unbind("<Leave>")
|
||||
|
||||
# Add new bindings if tooltips are enabled
|
||||
if self.state_var is None or self.state_var.get():
|
||||
self.widget.bind("<Enter>", self.show_tooltip)
|
||||
self.widget.bind("<Leave>", self.hide_tooltip)
|
||||
|
||||
def show_tooltip(self, event: Optional[Any] = None) -> None:
|
||||
"""Shows the tooltip"""
|
||||
if self.tooltip_window or not self.text:
|
||||
return
|
||||
|
||||
x: int
|
||||
y: int
|
||||
cx: int
|
||||
cy: int
|
||||
|
||||
x, y, cx, cy = self.widget.bbox("insert")
|
||||
x += self.widget.winfo_rootx() + self.x_offset
|
||||
y += self.widget.winfo_rooty() + self.y_offset
|
||||
|
||||
self.tooltip_window = tw = tk.Toplevel(self.widget)
|
||||
tw.wm_overrideredirect(True)
|
||||
tw.wm_geometry(f"+{x}+{y}")
|
||||
|
||||
label: tk.Label = tk.Label(
|
||||
tw,
|
||||
text=self.text,
|
||||
background="lightgreen",
|
||||
foreground="black",
|
||||
relief="solid",
|
||||
borderwidth=1,
|
||||
padx=5,
|
||||
pady=5,
|
||||
)
|
||||
label.grid()
|
||||
|
||||
self.tooltip_window.after(2200, lambda: tw.destroy())
|
||||
|
||||
def hide_tooltip(self, event: Optional[Any] = None) -> None:
|
||||
"""Hides the tooltip"""
|
||||
if self.tooltip_window:
|
||||
self.tooltip_window.destroy()
|
||||
self.tooltip_window = None
|
233
install
@ -1,233 +0,0 @@
|
||||
#!/bin/bash
|
||||
NORMAL='\033[0m'
|
||||
GREEN='\033[1;32m'
|
||||
RED='\033[31;1;42m'
|
||||
BLUE='\033[30;1;34m'
|
||||
|
||||
install_file_with(){
|
||||
clear
|
||||
mkdir -p ~/.config/wire_py && touch ~/.config/wire_py/keys && cp -u settings ~/.config/wire_py/ && \
|
||||
mkdir -p ~/.config/systemd/user && cp -u wg_start.service ~/.config/systemd/user/ && \
|
||||
systemctl --user enable wg_start.service >/dev/null 2>&1
|
||||
sudo cp -f org.sslcrypt.policy /usr/share/polkit-1/actions/
|
||||
if [ $? -ne 0 ]
|
||||
then
|
||||
systemctl --user disable wg_start.service
|
||||
rm -r ~/.config/wire_py && rm -r ~/.config/systemd
|
||||
exit 0
|
||||
else
|
||||
sudo apt install python3-tk && \
|
||||
sudo cp -fv wirepy.py start_wg.py wp_app_config.py common_tools.py ssl_encrypt.py ssl_decrypt.py /usr/local/bin/ && \
|
||||
sudo cp -fv match_found.py /usr/local/bin/ && \
|
||||
sudo cp -uR lx-icons /usr/share/icons/ && sudo cp -uR TK-Themes /usr/share/ && \
|
||||
sudo cp -u languages/de/*.mo /usr/share/locale/de/LC_MESSAGES/ && \
|
||||
sudo cp -fv Wire-Py.desktop /usr/share/applications/ && \
|
||||
sudo ln -sf /usr/local/bin/wirepy.py /usr/local/bin/wirepy
|
||||
sudo mkdir -p /usr/local/etc/ssl
|
||||
if [ ! -f /usr/local/etc/ssl/pwgk.pem ]
|
||||
then
|
||||
sudo openssl genrsa -out /usr/local/etc/ssl/pwgk.pem 4096
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
install_arch_d(){
|
||||
clear
|
||||
mkdir -p ~/.config/wire_py && touch ~/.config/wire_py/keys && cp -u settings ~/.config/wire_py/ && \
|
||||
mkdir -p ~/.config/systemd/user && cp -u wg_start.service ~/.config/systemd/user/ && \
|
||||
systemctl --user enable wg_start.service >/dev/null 2>&1
|
||||
sudo cp -f org.sslcrypt.policy /usr/share/polkit-1/actions/
|
||||
if [ $? -ne 0 ]
|
||||
then
|
||||
systemctl --user disable wg_start.service
|
||||
rm -r ~/.config/wire_py && rm -r ~/.config/systemd
|
||||
exit 0
|
||||
else
|
||||
sudo pacman -S --noconfirm tk python3 python-requests && \
|
||||
sudo cp -fv wirepy.py start_wg.py wp_app_config.py common_tools.py ssl_encrypt.py ssl_decrypt.py /usr/local/bin/ && \
|
||||
sudo cp -fv match_found.py /usr/local/bin/ && \
|
||||
sudo cp -uR lx-icons /usr/share/icons/ && sudo cp -uR TK-Themes /usr/share/ && \
|
||||
sudo cp -u languages/de/*.mo /usr/share/locale/de/LC_MESSAGES/ && \
|
||||
sudo cp -fv Wire-Py.desktop /usr/share/applications/ && \
|
||||
sudo ln -sf /usr/local/bin/wirepy.py /usr/local/bin/wirepy
|
||||
sudo mkdir -p /usr/local/etc/ssl
|
||||
if [ ! -f /usr/local/etc/ssl/pwgk.pem ]
|
||||
then
|
||||
sudo openssl genrsa -out /usr/local/etc/ssl/pwgk.pem 4096
|
||||
fi
|
||||
|
||||
fi
|
||||
}
|
||||
|
||||
install(){
|
||||
if grep -i 'debian' /etc/os-release > /dev/null 2>&1
|
||||
then
|
||||
groups > /tmp/isgroup
|
||||
if grep 'sudo' /tmp/isgroup
|
||||
then
|
||||
install_file_with
|
||||
else
|
||||
echo -e "$BLUE"The installer found that they are not in the group sudo.""
|
||||
echo -e "with "$RED"su -"$BLUE" "they can enter the root shell in which they then""
|
||||
echo -e "enter "$GREEN""usermod -aG sudo $USER.""$BLUE""
|
||||
echo -e ""after logging in from the system, they can then run Wire-Py install again." $NORMAL"
|
||||
read -n 1 -s -r -p $"Press Enter to exit"
|
||||
clear
|
||||
exit 0
|
||||
|
||||
fi
|
||||
|
||||
elif grep -i 'mint\|ubuntu\|pop|' /etc/os-release > /dev/null 2>&1
|
||||
then
|
||||
install_file_with
|
||||
|
||||
elif grep -i 'arch' /etc/os-release > /dev/null 2>&1
|
||||
then
|
||||
groups > /tmp/isgroup
|
||||
clear
|
||||
if grep 'wheel' /tmp/isgroup
|
||||
then
|
||||
install_arch_d
|
||||
else
|
||||
echo "The installer found that they are not in the group sudo."
|
||||
echo "The sudoers file must be edited with"
|
||||
echo -e "$RED""su -""$NORMAL"
|
||||
echo -e "$GREEN"""EDITOR=nano visudo"""$NORMAL"
|
||||
echo "Find the line:"
|
||||
echo "## Uncomment to allow members of group wheel to execute any command"
|
||||
echo "remove '#' on # %wheel ALL=(ALL) ALL and save the file"
|
||||
echo -e "then enter "$GREEN"gpasswd -a $USER wheel.""$NORMAL"
|
||||
echo "after logging in from the system, they can then run Wire-Py install again."
|
||||
read -n 1 -s -r -p $"Press Enter to exit"
|
||||
clear
|
||||
exit 0
|
||||
|
||||
fi
|
||||
|
||||
elif grep -i '|manjaro\|garuda\|endeavour|' /etc/os-release > /dev/null 2>&1
|
||||
then
|
||||
install_arch_d
|
||||
|
||||
elif grep -i 'fedora' /etc/os-release > /dev/null 2>&1
|
||||
then
|
||||
clear
|
||||
mkdir -p ~/.config/wire_py && touch ~/.config/wire_py/keys && cp -u settings ~/.config/wire_py/ && \
|
||||
mkdir -p ~/.config/systemd/user && cp -u wg_start.service ~/.config/systemd/user/ && \
|
||||
systemctl --user enable wg_start.service >/dev/null 2>&1
|
||||
sudo cp -f org.sslcrypt.policy /usr/share/polkit-1/actions/
|
||||
if [ $? -ne 0 ]
|
||||
then
|
||||
systemctl --user disable wg_start.service
|
||||
rm -r ~/.config/wire_py && rm -r ~/.config/systemd
|
||||
exit 0
|
||||
else
|
||||
sudo dnf install python3-tkinter -y
|
||||
sudo cp -fv wirepy.py start_wg.py wp_app_config.py common_tools.py ssl_encrypt.py ssl_decrypt.py /usr/local/bin/ && \
|
||||
sudo cp -fv match_found.py /usr/local/bin/ && \
|
||||
sudo cp -uR lx-icons /usr/share/icons/ && sudo cp -uR TK-Themes /usr/share/ && \
|
||||
sudo cp -u languages/de/*.mo /usr/share/locale/de/LC_MESSAGES/ && \
|
||||
sudo cp -fv Wire-Py.desktop /usr/share/applications/ && \
|
||||
sudo ln -sf /usr/local/bin/wirepy.py /usr/local/bin/wirepy
|
||||
sudo mkdir -p /usr/local/etc/ssl
|
||||
if [ ! -f /usr/local/etc/ssl/pwgk.pem ]
|
||||
then
|
||||
sudo openssl genrsa -out /usr/local/etc/ssl/pwgk.pem 4096
|
||||
fi
|
||||
|
||||
fi
|
||||
elif grep -i 'suse' /etc/os-release > /dev/null 2>&1
|
||||
then
|
||||
clear
|
||||
mkdir -p ~/.config/wire_py && touch ~/.config/wire_py/keys && cp -u settings ~/.config/wire_py/ && \
|
||||
mkdir -p ~/.config/systemd/user && cp -u wg_start.service ~/.config/systemd/user/ && \
|
||||
systemctl --user enable wg_start.service >/dev/null 2>&1
|
||||
sudo cp -f org.sslcrypt.policy /usr/share/polkit-1/actions/
|
||||
if [ $? -ne 0 ]
|
||||
then
|
||||
systemctl --user disable wg_start.service
|
||||
rm -r ~/.config/wire_py && rm -r ~/.config/systemd
|
||||
exit 0
|
||||
else
|
||||
sudo cp -fv wirepy.py start_wg.py wp_app_config.py common_tools.py ssl_encrypt.py ssl_decrypt.py /usr/local/bin/ && \
|
||||
sudo cp -fv match_found.py /usr/local/bin/ && \
|
||||
sudo cp -uR lx-icons /usr/share/icons/ && sudo cp -uR TK-Themes /usr/share/ && \
|
||||
sudo cp -u languages/de/*.mo /usr/share/locale/de/LC_MESSAGES/ && \
|
||||
sudo cp -fv Wire-Py.desktop /usr/share/applications/ && \
|
||||
sudo ln -sf /usr/local/bin/wirepy.py /usr/local/bin/wirepy
|
||||
sudo mkdir -p /usr/local/etc/ssl
|
||||
if [ ! -f /usr/local/etc/ssl/pwgk.pem ]
|
||||
then
|
||||
sudo openssl genrsa -out /usr/local/etc/ssl/pwgk.pem 4096
|
||||
fi
|
||||
if grep -i 'Tumbleweed' /etc/os-release > /dev/null 2>&1
|
||||
then
|
||||
sudo zypper install python313-tk
|
||||
else
|
||||
sudo zypper install python36-tk
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
else
|
||||
clear
|
||||
echo $"Your System could not be determined."
|
||||
echo
|
||||
read -n 1 -s -r -p $"Press Enter to exit"
|
||||
clear
|
||||
exit 0
|
||||
|
||||
fi
|
||||
#clear
|
||||
read -n 1 -s -r -p $"Press Enter to exit"
|
||||
clear
|
||||
|
||||
}
|
||||
|
||||
remove(){
|
||||
sudo rm -f /usr/local/bin/wirepy /usr/local/bin/wirepy.py /usr/local/bin/start_wg.py \
|
||||
/usr/local/bin/wp_app_config.py common_tools.py /usr/local/bin/ssl_encrypt.py \
|
||||
/usr/local/bin/ssl_decrypt.py /usr/local/bin/match_found.py
|
||||
if [ $? -ne 0 ]
|
||||
then
|
||||
exit 0
|
||||
else
|
||||
systemctl --user disable wg_start.service
|
||||
rm -r ~/.config/wire_py && rm -r ~/.config/systemd
|
||||
sudo rm /usr/share/applications/Wire-Py.desktop
|
||||
sudo rm /usr/share/locale/de/LC_MESSAGES/languages/de/wirepy.mo
|
||||
sudo rm -r /usr/local/etc/ssl
|
||||
which syncpy >/dev/null
|
||||
if [ $? -ne 0 ]
|
||||
then
|
||||
sudo rm -r /usr/share/icons/lx-icons && sudo rm -r /usr/share/TK-Themes
|
||||
|
||||
fi
|
||||
|
||||
echo
|
||||
read -p "Press Enter to exit..."
|
||||
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
which wirepy >/dev/null
|
||||
if [ $? -eq 0 ]
|
||||
then
|
||||
echo "Do you want to update/reinstall or uninstall wirepy?"
|
||||
echo
|
||||
echo "Update/reinstall: press y, uninstall press r"
|
||||
echo
|
||||
read -n 1 -s -r -p "Cancel with any other key..." result
|
||||
case $result in
|
||||
[y]* ) clear; install; exit;;
|
||||
[Y]* ) clear; install; exit;;
|
||||
[j]* ) clear; install; exit;;
|
||||
[J]* ) clear; install; exit;;
|
||||
[r]* ) clear; remove; exit;;
|
||||
[R]* ) clear; remove; exit;;
|
||||
esac
|
||||
clear
|
||||
else
|
||||
install
|
||||
|
||||
fi
|
BIN
lx-icons/128/download.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
lx-icons/128/download_error.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
lx-icons/128/log.png
Normal file
After Width: | Height: | Size: 4.5 KiB |
BIN
lx-icons/256/download.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
lx-icons/256/download_error.png
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
lx-icons/256/log.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
lx-icons/32/download.png
Normal file
After Width: | Height: | Size: 984 B |
BIN
lx-icons/32/download_error.png
Normal file
After Width: | Height: | Size: 657 B |
BIN
lx-icons/32/log.png
Normal file
After Width: | Height: | Size: 906 B |
Before Width: | Height: | Size: 5.8 KiB |
BIN
lx-icons/48/download.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
lx-icons/48/download_error.png
Normal file
After Width: | Height: | Size: 906 B |
BIN
lx-icons/48/log.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
lx-icons/64/download.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
lx-icons/64/download_error.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
lx-icons/64/log.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
@ -47,7 +47,8 @@ def search_string_in_directory(
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Script only for use to compare the private key in the Network configurations to avoid errors with the network manager."
|
||||
description="Script only for use to compare the private key in the"
|
||||
"Network configurations to avoid errors with the network manager."
|
||||
)
|
||||
parser.add_argument("search_string", help="Search string")
|
||||
args = parser.parse_args()
|
||||
|
@ -5,7 +5,7 @@ from pathlib import Path
|
||||
import pwd
|
||||
import shutil
|
||||
from subprocess import CompletedProcess, run
|
||||
from wp_app_config import AppConfig, logging
|
||||
from shared_libs.wp_app_config import AppConfig, logging
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--user", required=True, help="Username of the target file system")
|
||||
|
@ -6,7 +6,7 @@ from pathlib import Path
|
||||
import pwd
|
||||
import shutil
|
||||
from subprocess import CompletedProcess, run
|
||||
from wp_app_config import AppConfig, logging
|
||||
from shared_libs.wp_app_config import AppConfig, logging
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--user", required=True, help="Username of the target file system")
|
||||
|
@ -2,13 +2,13 @@
|
||||
"""
|
||||
This script belongs to wirepy and is for the auto start of the tunnel
|
||||
"""
|
||||
|
||||
import logging
|
||||
from subprocess import CompletedProcess, run
|
||||
from wp_app_config import AppConfig, logging
|
||||
from common_tools import ConfigManager
|
||||
from shared_libs.wp_app_config import AppConfig
|
||||
from shared_libs.common_tools import ConfigManager, LogConfig
|
||||
|
||||
ConfigManager.init(AppConfig.SETTINGS_FILE)
|
||||
|
||||
LogConfig.logger(ConfigManager.get("logfile"))
|
||||
if ConfigManager.get("autostart") != "off":
|
||||
process: CompletedProcess[str] = run(
|
||||
["nmcli", "connection", "up", ConfigManager.get("autostart")],
|
||||
|
230
tunnel.py
Normal file
@ -0,0 +1,230 @@
|
||||
#!/usr/bin/python3
|
||||
import logging
|
||||
import getpass
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
from subprocess import run, CompletedProcess
|
||||
import secrets
|
||||
from shared_libs.wp_app_config import AppConfig, Msg
|
||||
from shared_libs.common_tools import LxTools, CryptoUtil
|
||||
|
||||
# Translate
|
||||
_ = AppConfig.setup_translations()
|
||||
|
||||
|
||||
class Tunnel:
|
||||
"""
|
||||
Class of Methods for Wire-Py
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def parse_files_to_dictionary(
|
||||
directory: Path = None, filepath: str = None, content: str = None
|
||||
) -> tuple[dict, str] | dict | None:
|
||||
data = {}
|
||||
|
||||
if filepath is not None:
|
||||
filepath = Path(filepath)
|
||||
try:
|
||||
content = filepath.read_text()
|
||||
|
||||
# parse the content
|
||||
address_line = next(
|
||||
line for line in content.splitlines() if line.startswith("Address")
|
||||
)
|
||||
dns_line = next(
|
||||
line for line in content.splitlines() if line.startswith("DNS")
|
||||
)
|
||||
endpoint_line = next(
|
||||
line for line in content.splitlines() if line.startswith("Endpoint")
|
||||
)
|
||||
private_key_line = next(
|
||||
line
|
||||
for line in content.splitlines()
|
||||
if line.startswith("PrivateKey")
|
||||
)
|
||||
|
||||
content = secrets.token_bytes(len(content))
|
||||
|
||||
# extract the values
|
||||
address = address_line.split("=")[1].strip()
|
||||
dns = dns_line.split("=")[1].strip()
|
||||
endpoint = endpoint_line.split("=")[1].strip()
|
||||
private_key = private_key_line.split("=")[1].strip()
|
||||
|
||||
# Shorten the tunnel name to the maximum allowed length if it exceeds 12 characters.
|
||||
original_stem = filepath.stem
|
||||
truncated_stem = (
|
||||
original_stem[-12:] if len(original_stem) > 12 else original_stem
|
||||
)
|
||||
|
||||
# save in the dictionary
|
||||
data[truncated_stem] = {
|
||||
"Address": address,
|
||||
"DNS": dns,
|
||||
"Endpoint": endpoint,
|
||||
"PrivateKey": private_key,
|
||||
}
|
||||
|
||||
content = secrets.token_bytes(len(content))
|
||||
|
||||
except StopIteration:
|
||||
pass
|
||||
|
||||
elif directory is not None:
|
||||
|
||||
if not directory.exists() or not directory.is_dir():
|
||||
logging.error(
|
||||
"Temp directory does not exist or is not a directory.",
|
||||
exc_info=True,
|
||||
)
|
||||
return None
|
||||
|
||||
# Get a list of all files in the directory
|
||||
files = [file for file in AppConfig.TEMP_DIR.iterdir() if file.is_file()]
|
||||
|
||||
# Search for the string in the files
|
||||
for file in files:
|
||||
try:
|
||||
content = file.read_text()
|
||||
# parse the content
|
||||
address_line = next(
|
||||
line
|
||||
for line in content.splitlines()
|
||||
if line.startswith("Address")
|
||||
)
|
||||
dns_line = next(
|
||||
line for line in content.splitlines() if line.startswith("DNS")
|
||||
)
|
||||
endpoint_line = next(
|
||||
line
|
||||
for line in content.splitlines()
|
||||
if line.startswith("Endpoint")
|
||||
)
|
||||
|
||||
# extract values
|
||||
address = address_line.split("=")[1].strip()
|
||||
dns = dns_line.split("=")[1].strip()
|
||||
endpoint = endpoint_line.split("=")[1].strip()
|
||||
|
||||
# save values to dictionary
|
||||
data[file.stem] = {
|
||||
"Address": address,
|
||||
"DNS": dns,
|
||||
"Endpoint": endpoint,
|
||||
}
|
||||
|
||||
except Exception:
|
||||
# Ignore errors and continue to the next file
|
||||
continue
|
||||
if content is not None:
|
||||
content = secrets.token_bytes(len(content))
|
||||
if filepath is not None:
|
||||
return data, truncated_stem
|
||||
else:
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def get_active() -> str:
|
||||
"""
|
||||
Shows the Active Tunnel
|
||||
"""
|
||||
active = None
|
||||
try:
|
||||
process: CompletedProcess[str] = run(
|
||||
["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show", "--active"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
active = next(
|
||||
line.split(":")[0].strip()
|
||||
for line in process.stdout.splitlines()
|
||||
if line.endswith("wireguard")
|
||||
)
|
||||
|
||||
if process.stderr and "error" in process.stderr.lower():
|
||||
logging.error(f"Error output on nmcli: {process.stderr}")
|
||||
|
||||
except StopIteration:
|
||||
active = None
|
||||
except Exception as e:
|
||||
logging.error(f"Error on nmcli: {e}")
|
||||
active = None
|
||||
|
||||
return active if active is not None else ""
|
||||
|
||||
@staticmethod
|
||||
def export() -> bool | None:
|
||||
"""
|
||||
This will export the tunnels.
|
||||
A zipfile with the current date and time is created
|
||||
in the user's home directory with the correct right
|
||||
"""
|
||||
now_time: datetime = datetime.now()
|
||||
now_datetime: str = now_time.strftime("wg-exp-%m-%d-%Y-%H:%M")
|
||||
|
||||
try:
|
||||
AppConfig.ensure_directories()
|
||||
CryptoUtil.decrypt(getpass.getuser())
|
||||
if len([file.name for file in AppConfig.TEMP_DIR.glob("*.conf")]) == 0:
|
||||
|
||||
LxTools.msg_window(
|
||||
AppConfig.IMAGE_PATHS["icon_info"],
|
||||
AppConfig.IMAGE_PATHS["icon_msg"],
|
||||
Msg.STR["sel_tl"],
|
||||
Msg.STR["tl_first"],
|
||||
)
|
||||
return False
|
||||
else:
|
||||
wg_tar: str = f"{AppConfig.BASE_DIR}/{now_datetime}"
|
||||
try:
|
||||
shutil.make_archive(wg_tar, "zip", AppConfig.TEMP_DIR)
|
||||
with zipfile.ZipFile(f"{wg_tar}.zip", "r") as zf:
|
||||
if zf.namelist():
|
||||
|
||||
LxTools.msg_window(
|
||||
AppConfig.IMAGE_PATHS["icon_info"],
|
||||
AppConfig.IMAGE_PATHS["icon_vpn"],
|
||||
Msg.STR["exp_succ"],
|
||||
Msg.STR["exp_in_home"],
|
||||
)
|
||||
else:
|
||||
logging.error(
|
||||
"There was a mistake at creating the Zip file. File is empty."
|
||||
)
|
||||
LxTools.msg_window(
|
||||
AppConfig.IMAGE_PATHS["icon_error"],
|
||||
AppConfig.IMAGE_PATHS["icon_msg"],
|
||||
Msg.STR["exp_err"],
|
||||
Msg.STR["exp_zip"],
|
||||
)
|
||||
return False
|
||||
return True
|
||||
except PermissionError:
|
||||
logging.error(
|
||||
f"Permission denied when creating archive in {wg_tar}"
|
||||
)
|
||||
return False
|
||||
|
||||
except zipfile.BadZipFile as e:
|
||||
logging.error(f"Invalid ZIP file: {e}")
|
||||
return False
|
||||
except TypeError:
|
||||
pass
|
||||
except Exception as e:
|
||||
logging.error(f"Export failed: {str(e)}")
|
||||
LxTools.msg_window(
|
||||
AppConfig.IMAGE_PATHS["icon_error"],
|
||||
AppConfig.IMAGE_PATHS["icon_msg"],
|
||||
Msg.STR["exp_err"],
|
||||
Msg.STR["exp_try"],
|
||||
)
|
||||
return False
|
||||
|
||||
finally:
|
||||
LxTools.clean_files(AppConfig.TEMP_DIR)
|
||||
AppConfig.ensure_directories()
|
@ -1,11 +0,0 @@
|
||||
[Unit]
|
||||
Description=Automatic Tunnel Start
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStartPre=/bin/sleep 5
|
||||
ExecStart=/usr/local/bin/start_wg.py
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
37
wirepy.py
@ -2,7 +2,7 @@
|
||||
"""
|
||||
this script is a simple GUI for managing Wireguard Tunnels
|
||||
"""
|
||||
|
||||
import logging
|
||||
import getpass
|
||||
import shutil
|
||||
import sys
|
||||
@ -11,21 +11,19 @@ import webbrowser
|
||||
from pathlib import Path
|
||||
from subprocess import CompletedProcess, run
|
||||
from tkinter import TclError, filedialog, ttk
|
||||
from tunnel import Tunnel
|
||||
|
||||
from common_tools import (
|
||||
from shared_libs.gitea import GiteaUpdate
|
||||
from shared_libs.common_tools import (
|
||||
LxTools,
|
||||
CryptoUtil,
|
||||
LogConfig,
|
||||
ConfigManager,
|
||||
ThemeManager,
|
||||
CryptoUtil,
|
||||
GiteaUpdate,
|
||||
Tunnel,
|
||||
Tooltip,
|
||||
LxTools,
|
||||
)
|
||||
from wp_app_config import AppConfig, Msg, logging
|
||||
|
||||
AppConfig.ensure_directories()
|
||||
AppConfig.create_default_settings()
|
||||
CryptoUtil.decrypt(getpass.getuser())
|
||||
from shared_libs.wp_app_config import AppConfig, Msg
|
||||
|
||||
|
||||
class Wirepy(tk.Tk):
|
||||
@ -169,7 +167,11 @@ class FrameWidgets(ttk.Frame):
|
||||
self.settings.add_command(
|
||||
label=self.theme_label.get(), command=self.on_theme_toggle
|
||||
)
|
||||
|
||||
# Logviewer Menu
|
||||
self.settings.add_command(
|
||||
label="Log Viewer",
|
||||
command=lambda: run(["logviewer", "--modul=wp_app_config"]),
|
||||
)
|
||||
# About BTN Menu / Label
|
||||
self.about_btn = ttk.Button(
|
||||
self.menu_frame, text=_("About"), style="Toolbutton", command=self.about
|
||||
@ -451,12 +453,7 @@ class FrameWidgets(ttk.Frame):
|
||||
self.download.add_command(
|
||||
label=_("Download"),
|
||||
command=lambda: GiteaUpdate.download(
|
||||
f"{AppConfig.DOWNLOAD_URL}/{res}.zip",
|
||||
res,
|
||||
AppConfig.IMAGE_PATHS["icon_info"],
|
||||
AppConfig.IMAGE_PATHS["icon_vpn"],
|
||||
AppConfig.IMAGE_PATHS["icon_error"],
|
||||
AppConfig.IMAGE_PATHS["icon_msg"],
|
||||
f"{AppConfig.DOWNLOAD_URL}/{res}.zip", res
|
||||
),
|
||||
)
|
||||
|
||||
@ -1145,10 +1142,14 @@ class FrameWidgets(ttk.Frame):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
AppConfig.ensure_directories()
|
||||
AppConfig.create_default_settings()
|
||||
CryptoUtil.decrypt(getpass.getuser(), AppConfig.CONFIG_DIR)
|
||||
_ = AppConfig.setup_translations()
|
||||
LxTools.sigi(AppConfig.TEMP_DIR)
|
||||
|
||||
window = Wirepy()
|
||||
LogConfig.logger(ConfigManager.get("logfile"))
|
||||
"""
|
||||
the hidden files are hidden in Filedialog
|
||||
"""
|
||||
|
17
wp_app_config.py
Normal file → Executable file
@ -29,16 +29,10 @@ class AppConfig:
|
||||
"""
|
||||
|
||||
# Logging
|
||||
LOG_DIR = Path.home() / ".local/share/wirepy"
|
||||
LOG_DIR = Path.home() / ".local/share/lxlogs"
|
||||
Path(LOG_DIR).mkdir(parents=True, exist_ok=True)
|
||||
LOG_FILE_PATH = LOG_DIR / "wirepy.log"
|
||||
|
||||
logging.basicConfig(
|
||||
filename=f"{LOG_FILE_PATH}",
|
||||
level=logging.ERROR,
|
||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||
)
|
||||
|
||||
# Localization
|
||||
APP_NAME: str = "wirepy"
|
||||
LOCALE_DIR: Path = Path("/usr/share/locale/")
|
||||
@ -58,6 +52,7 @@ class AppConfig:
|
||||
"# Theme": "dark",
|
||||
"# Tooltips": True,
|
||||
"# Autostart": "off",
|
||||
"# Logfile": LOG_FILE_PATH,
|
||||
}
|
||||
|
||||
# Updates
|
||||
@ -69,6 +64,7 @@ class AppConfig:
|
||||
# UI configuration
|
||||
UI_CONFIG: Dict[str, Any] = {
|
||||
"window_title": "Wire-Py",
|
||||
"window_title2": "LogViewer",
|
||||
"window_size": (600, 383),
|
||||
"font_family": "Ubuntu",
|
||||
"font_size": 11,
|
||||
@ -94,6 +90,7 @@ class AppConfig:
|
||||
"icon_stop": "/usr/share/icons/lx-icons/48/wg_vpn-stop.png",
|
||||
"icon_info": "/usr/share/icons/lx-icons/64/info.png",
|
||||
"icon_error": "/usr/share/icons/lx-icons/64/error.png",
|
||||
"icon_log": "/usr/share/icons/lx-icons/48/log.png",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@ -160,6 +157,12 @@ class AppConfig:
|
||||
if process.stderr:
|
||||
logging.error(f"{process.stderr} Code: {process.returncode}", exc_info=True)
|
||||
|
||||
@classmethod
|
||||
def ensure_log(cls) -> None:
|
||||
"""Ensures that the log file exists"""
|
||||
if not cls.LOG_FILE_PATH.exists():
|
||||
cls.LOG_FILE_PATH.touch()
|
||||
|
||||
|
||||
# here is initializing the class for translation strings
|
||||
_ = AppConfig.setup_translations()
|
||||
|