7 Commits

28 changed files with 454 additions and 1312 deletions

View File

@ -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

View File

@ -1,865 +0,0 @@
""" Classes Method and Functions for lx Apps """
import os
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, List
import zipfile
from datetime import datetime
from pathlib import Path
from tkinter import ttk, Toplevel
from wp_app_config import AppConfig, Msg
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() -> str:
"""
Starts SSL dencrypt
"""
crypted_tunnel = [str(file) for file in AppConfig.CONFIG_DIR.glob("*.dat")]
if crypted_tunnel == []:
return
process: CompletedProcess[str] = subprocess.run(
["pkexec", "/usr/local/bin/ssl_decrypt.py"],
capture_output=True,
text=True,
check=False,
)
# Output from Openssl Error
if process.stderr:
print(process.stderr)
if process.returncode == 0:
print("Files successfully decrypted...")
else:
print(f"Error process decrypt: Code {process.returncode}")
@staticmethod
def encrypt() -> str:
"""
Starts SSL encryption
"""
process: CompletedProcess[str] = subprocess.run(
["pkexec", "/usr/local/bin/ssl_encrypt.py"],
capture_output=True,
text=True,
check=False,
)
# Output from Openssl Error
if process.stderr:
print(process.stderr)
if process.returncode == 0:
print("Files successfully encrypted...")
else:
print(f"Error process encrypt: Code {process.returncode}")
@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[bool] = 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
print(
f"Unexpected output from the external script:\nSTDOUT: {process.stdout}\nSTDERR: {process.stderr}"
)
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:
return False
return True
class LxTools(tk.Tk):
"""
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 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: WIDTHxHEIGHT+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 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 (subprocess.SubprocessError, 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 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 get_file_name(path: Path, i: int = 5) -> List[str]:
"""
Recursively searches the specified path for files and returns a list of filenames,
with the last 'i' characters of each filename removed.
This method is useful for obtaining filenames without specific file extensions,
e.g., to remove '.conf' from Wireguard configuration files.
Args:
path (Path): The directory path to search
i (int, optional): Number of characters to remove from the end of each filename.
Default is 5, which typically corresponds to the length of '.conf'.
Returns:
List[str]: A list of filenames without the last 'i' characters
Example:
If path contains files like 'tunnel1.conf', 'tunnel2.conf' and i=5,
the method returns ['tunnel1', 'tunnel2'].
"""
lists_file = list(path.rglob("*"))
lists_file = [conf_file.name[:-i] for conf_file in lists_file]
return lists_file
@staticmethod
def get_username() -> str:
"""
Returns the username of the logged-in user,
even if the script is running with root privileges.
"""
try:
result = subprocess.run(
["logname"],
stdout=subprocess.PIPE,
text=True,
check=True,
)
if result.returncode != 0:
pass
return result.stdout.strip()
except subprocess.CalledProcessError:
pass
@staticmethod
def clean_files(TEMP_DIR: Path = None, file: Path = None) -> None:
"""
method that can be added after need to delete a folder and a file when quitting.
Args:
:param file: default None
:param AppConfig.TEMP_DIR: default None
"""
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
:argument AppConfig.IMAGE_PATHS["icon_info"] = Image for TK window which is displayed to the left of the text
:argument 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)
# Lade das erste Bild für das Fenster
try:
msg.img = tk.PhotoImage(file=image_path)
msg.i_window = tk.Label(msg, image=msg.img)
except Exception as e:
print(f"Fehler beim Laden des Fensterbildes: {e}")
msg.i_window = tk.Label(msg, text="Bild nicht gefunden")
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)
# Lade das Icon für das Fenster
try:
icon = tk.PhotoImage(file=image_path2)
msg.iconphoto(True, icon)
except Exception as e:
print(f"Fehler beim Laden des Fenstericons: {e}")
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
print(
f"\nSignal {signal_name} {signum} received. => Aborting with exit code {exit_code}."
)
LxTools.clean_files(file_path, file)
print("Breakdown by user...")
sys.exit(exit_code)
else:
print(f"Signal {signum} received and ignored.")
LxTools.clean_files(file_path, file)
print("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
) -> dict | str | 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():
print("Temp directory does not exist or is not a directory.")
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 active() -> str:
"""
Shows the Active Tunnel
"""
active = (
os.popen('nmcli con show --active | grep -iPo "(.*)(wireguard)"')
.read()
.split()
)
if not active:
active = ""
else:
active = active[0]
return active
@staticmethod
def list() -> List[str]:
"""
Returns a list of Wireguard tunnel names
"""
AppConfig.TEMP_DIR: Path = Path("/tmp/tlecdcwg/")
wg_s: List[str] = os.listdir(AppConfig.TEMP_DIR)
return wg_s
@staticmethod
def export(
image_path: Path = None,
image_path2: Path = None,
image_path3: Path = None,
image_path4: Path = None,
title: Dict = None,
window_msg: Dict = None,
) -> 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
Args:
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
AppConfig.IMAGE_PATHS["icon_error"]: Image for TK window which is displayed to the left of the text
AppConfig.IMAGE_PATHS["icon_msg"]: Image for Task Icon
"""
now_time: datetime = datetime.now()
now_datetime: str = now_time.strftime("wg-exp-%m-%d-%Y-%H:%M")
tl: List[str] = Tunnel.list()
try:
if len(tl) != 0:
wg_tar: str = f"{Path.home()}/{now_datetime}"
shutil.copytree("/tmp/tlecdcwg/", "/tmp/wire_py", dirs_exist_ok=True)
source: Path = Path("/tmp/wire_py")
shutil.make_archive(wg_tar, "zip", source)
shutil.rmtree(source)
with zipfile.ZipFile(f"{wg_tar}.zip", "r") as zf:
if len(zf.namelist()) != 0:
LxTools.msg_window(
AppConfig.IMAGE_PATHS["icon_info"],
AppConfig.IMAGE_PATHS["icon_vpn"],
Msg.STR["exp_succ"],
Msg.STR["exp_in_home"],
)
else:
LxTools.msg_window(
AppConfig.IMAGE_PATHS["icon_error"],
AppConfig.IMAGE_PATHS["icon_msg"],
Msg.STR["exp_err"],
Msg.STR["exp_try"],
)
else:
LxTools.msg_window(
AppConfig.IMAGE_PATHS["icon_info"],
AppConfig.IMAGE_PATHS["icon_msg"],
Msg.STR["sel_tl"],
Msg.STR["tl_first"],
)
except TypeError:
pass
# ConfigManager with caching
class ConfigManager:
"""
Universal class for managing configuration files with caching.
Can be reused in different projects.
"""
_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:
@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
Args:
urld: Download URL
res: Result filename
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
AppConfig.IMAGE_PATHS["icon_error"]: Image for TK window which is displayed to the left of the text
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 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
View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
lx-icons/128/log.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
lx-icons/256/download.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
lx-icons/256/log.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
lx-icons/32/download.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 984 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 657 B

BIN
lx-icons/32/log.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

BIN
lx-icons/48/download.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 B

BIN
lx-icons/48/log.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
lx-icons/64/download.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
lx-icons/64/log.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -42,13 +42,13 @@ def search_string_in_directory(
except Exception:
continue # Skip files that cause errors
# Invert the logic: return False if string is found, True otherwise
return result
def main() -> bool:
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()

View File

@ -1,9 +0,0 @@
# Configuration
on
# Theme
dark
# Tooltips
True
# Autostart
off

View File

@ -1,19 +1,30 @@
#!/usr/bin/python3
""" This Script decrypt Wireguard files for Wirepy users """
import argparse
from pathlib import Path
import pwd
import shutil
from subprocess import CompletedProcess
import subprocess
from wp_app_config import AppConfig
from subprocess import CompletedProcess, run
from shared_libs.wp_app_config import AppConfig, logging
log_name = AppConfig.USER_FILE.read_text().strip()
parser = argparse.ArgumentParser()
parser.add_argument("--user", required=True, help="Username of the target file system")
args = parser.parse_args()
keyfile: Path = Path(f"/home/{log_name}/.config/wire_py/pbwgk.pem")
path_of_crypted_tunnel: Path = Path(f"/home/{log_name}/.config/wire_py")
try:
# Retrieve UID and GID
user_info = pwd.getpwnam(args.user)
uid = user_info.pw_uid # User ID (e.g., 1000)
gid = user_info.pw_gid # Group ID (e.g., 1000)
except KeyError:
logging.error(f"User '{args.user}' not found.", exc_info=True)
exit(1)
keyfile: Path = Path(f"/home/{args.user}/.config/wire_py/pbwgk.pem")
path_of_crypted_tunnel: Path = Path(f"/home/{args.user}/.config/wire_py")
if not keyfile.is_file():
process: CompletedProcess[str] = subprocess.run(
process: CompletedProcess[str] = run(
[
"openssl",
"rsa",
@ -29,14 +40,16 @@ if not keyfile.is_file():
text=True,
check=False,
)
print(process.stdout)
if process.returncode == 0:
print("Public key generated successfully.")
else:
print(f"Error with the following code... {process.returncode}")
shutil.chown(keyfile, 1000, 1000)
if AppConfig.PUBLICKEY.exists:
if process.returncode == 0:
logging.info("Public key generated successfully.", exc_info=True)
else:
logging.error(
f"Error with the following code... {process.returncode}", exc_info=True
)
shutil.chown(keyfile, uid, gid)
if AppConfig.PUBLICKEY.exists():
crypted__tunnel = [str(file) for file in path_of_crypted_tunnel.glob("*.dat")]
@ -44,7 +57,7 @@ if AppConfig.PUBLICKEY.exists:
base_name = Path(tunnel_path).stem
process: CompletedProcess[str] = subprocess.run(
process: CompletedProcess[str] = run(
[
"openssl",
"pkeyutl",
@ -60,17 +73,12 @@ if AppConfig.PUBLICKEY.exists:
text=True,
check=False,
)
shutil.chown(f"{AppConfig.TEMP_DIR}/{base_name}.conf", 1000, 1000)
print(f"Processing of the file: {tunnel_path}")
if process.stdout:
print(process.stdout)
shutil.chown(f"{AppConfig.TEMP_DIR}/{base_name}.conf", uid, gid)
logging.info(f"Processing of the file: {tunnel_path}", exc_info=True)
# Output from Openssl Error
if process.stderr:
print("(Error):", process.stderr)
if process.returncode == 0:
print(f"File {base_name}.dat successfully decrypted.")
else:
print(f"Error by {tunnel_path}: Code: {process.returncode}")
logging.error(
f"{process.stderr} Error by [{tunnel_path}] Code: {process.returncode}",
exc_info=True,
)

View File

@ -1,20 +1,33 @@
#!/usr/bin/python3
""" This Script encrypt Wireguardfiles for Wirepy users for more Security """
import argparse
from pathlib import Path
import pwd
import shutil
import subprocess
from subprocess import CompletedProcess
from wp_app_config import AppConfig
from subprocess import CompletedProcess, run
from shared_libs.wp_app_config import AppConfig, logging
log_name = AppConfig.USER_FILE.read_text().strip()
parser = argparse.ArgumentParser()
parser.add_argument("--user", required=True, help="Username of the target file system")
args = parser.parse_args()
keyfile: Path = Path(f"/home/{log_name}/.config/wire_py/pbwgk.pem")
try:
# Retrieve UID and GID
user_info = pwd.getpwnam(args.user)
uid = user_info.pw_uid # User ID (e.g., 1000)
gid = user_info.pw_gid # Group ID (e.g., 1000)
except KeyError:
logging.error(f"User '{args.user}' not found.", exc_info=True)
exit(1)
target: Path = Path(f"/home/{log_name}/.config/wire_py/")
keyfile: Path = Path(f"/home/{args.user}/.config/wire_py/pbwgk.pem")
target: Path = Path(f"/home/{args.user}/.config/wire_py/")
if not keyfile.is_file():
process: CompletedProcess[str] = subprocess.run(
process: CompletedProcess[str] = run(
[
"openssl",
"rsa",
@ -31,19 +44,14 @@ if not keyfile.is_file():
check=False,
)
if process.stdout:
print(process.stdout)
# Output from Openssl Error
if process.stderr:
print("(Error):", process.stderr)
logging.error(f"{process.stderr} Code: {process.returncode}", exc_info=True)
if process.returncode == 0:
print("Public key generated successfully.")
else:
print(f"Error generate Publickey: Code: {process.returncode}")
logging.info("Public key generated successfully.", exc_info=True)
shutil.chown(keyfile, 1000, 1000)
shutil.chown(keyfile, uid, gid)
# any() get True when directory is not empty
if AppConfig.TEMP_DIR.exists() and any(AppConfig.TEMP_DIR.iterdir()):
@ -51,7 +59,7 @@ if AppConfig.TEMP_DIR.exists() and any(AppConfig.TEMP_DIR.iterdir()):
for config_file in clear_files:
base_name = Path(config_file).stem
process: CompletedProcess[str] = subprocess.run(
process: CompletedProcess[str] = run(
[
"openssl",
"pkeyutl",
@ -69,13 +77,6 @@ if AppConfig.TEMP_DIR.exists() and any(AppConfig.TEMP_DIR.iterdir()):
check=False,
)
print(f"Processing of the file: {config_file}")
# Output from Openssl Error
if process.stderr:
print("(Error):", process.stderr)
if process.returncode == 0:
print(f"File {base_name}.dat successfully encrypted.")
else:
print(f"Error by {config_file}: Code: {process.returncode}")
logging.error(process.stderr, exc_info=True)

View File

@ -2,16 +2,15 @@
"""
This script belongs to wirepy and is for the auto start of the tunnel
"""
import subprocess
from subprocess import CompletedProcess
from wp_app_config import AppConfig
from common_tools import ConfigManager
import logging
from subprocess import CompletedProcess, run
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] = subprocess.run(
process: CompletedProcess[str] = run(
["nmcli", "connection", "up", ConfigManager.get("autostart")],
capture_output=True,
text=True,
@ -19,7 +18,7 @@ if ConfigManager.get("autostart") != "off":
)
# Output from start_wg error
if process.stderr:
print(process.stderr) # this is for the error, later on logfile
logging.error(process.stderr, exc_info=True)
else:
pass

230
tunnel.py Normal file
View 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()

View File

@ -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

206
wirepy.py
View File

@ -2,32 +2,28 @@
"""
this script is a simple GUI for managing Wireguard Tunnels
"""
import logging
import getpass
import shutil
import subprocess
import sys
import tkinter as tk
import webbrowser
from pathlib import Path
from subprocess import CompletedProcess
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
AppConfig.USER_FILE.write_text(getpass.getuser())
AppConfig.ensure_directories()
AppConfig.create_default_settings()
CryptoUtil.decrypt()
from shared_libs.wp_app_config import AppConfig, Msg
class Wirepy(tk.Tk):
@ -171,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
@ -179,7 +179,7 @@ class FrameWidgets(ttk.Frame):
self.about_btn.grid(column=2, columnspan=2, row=0)
self.readme = tk.Menu(self)
self.a = Tunnel.active()
self.a = Tunnel.get_active()
# Label Frame 1
self.lb_frame_btn_lbox = ttk.Frame(self)
@ -244,7 +244,7 @@ class FrameWidgets(ttk.Frame):
self.tl = Tunnel.parse_files_to_dictionary(directory=AppConfig.TEMP_DIR)
LxTools.clean_files(AppConfig.TEMP_DIR, file=None)
AppConfig.ensure_directories()
# self.tl = LxTools.get_file_name(AppConfig.TEMP_DIR)
for tunnels, values in self.tl.items():
self.l_box.insert("end", tunnels)
self.l_box.update()
@ -294,14 +294,7 @@ class FrameWidgets(ttk.Frame):
self.btn_exp = ttk.Button(
self.lb_frame_btn_lbox,
image=self.exp_pic,
command=lambda: Tunnel.export(
AppConfig.IMAGE_PATHS["icon_info"],
AppConfig.IMAGE_PATHS["icon_vpn"],
AppConfig.IMAGE_PATHS["icon_error"],
AppConfig.IMAGE_PATHS["icon_msg"],
Msg.STR["sel_tl"],
Msg.STR["tl_first"],
),
command=lambda: Tunnel.export(),
padding=0,
)
@ -409,7 +402,7 @@ class FrameWidgets(ttk.Frame):
# Update the labels based on the result
def update_ui_for_update(self, res):
"""Update UI elements based on update check result"""
"""Update UI elements based on an update check result"""
# First, remove the update button if it exists to avoid conflicts
if hasattr(self, "update_btn"):
self.update_btn.grid_forget()
@ -421,13 +414,13 @@ class FrameWidgets(ttk.Frame):
self.update_tooltip.set(_("Updates you have disabled"))
# Clear the foreground color as requested
self.update_foreground.set("")
# Set tooltip for the label
# Set the tooltip for the label
Tooltip(self.updates_lb, self.update_tooltip.get(), self.tooltip_state)
elif res == "No Internet Connection!":
self.update_label.set(_("No Server Connection!"))
self.update_foreground.set("red")
# Set tooltip for "No Server Connection"
# Set the tooltip for "No Server Connection"
Tooltip(
self.updates_lb,
_("Could not connect to update server"),
@ -438,7 +431,7 @@ class FrameWidgets(ttk.Frame):
self.update_label.set(_("No Updates"))
self.update_tooltip.set(_("Congratulations! Wire-Py is up to date"))
self.update_foreground.set("")
# Set tooltip for the label
# Set the tooltip for the label
Tooltip(self.updates_lb, self.update_tooltip.get(), self.tooltip_state)
else:
@ -460,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
),
)
@ -475,7 +463,7 @@ class FrameWidgets(ttk.Frame):
a tk.Toplevel window
"""
def link_btn() -> str | None:
def link_btn() -> None:
webbrowser.open("https://git.ilunix.de/punix/Wire-Py")
msg_t = _(
@ -514,7 +502,7 @@ class FrameWidgets(ttk.Frame):
AppConfig.UPDATE_URL, AppConfig.VERSION, "on"
)
# Make sure UI is updated regardless of previous state
# Make sure the UI is updated regardless of the previous state
if hasattr(self, "update_btn"):
self.update_btn.grid_forget()
if hasattr(self, "updates_lb"):
@ -523,7 +511,7 @@ class FrameWidgets(ttk.Frame):
# Now update the UI with the fresh result
self.update_ui_for_update(res)
except Exception as e:
print(f"Error checking for updates: {e}")
logging.error(f"Error checking for updates: {e}", exc_info=True)
# Fallback to a default message if there's an error
self.update_ui_for_update("No Internet Connection!")
@ -538,7 +526,13 @@ class FrameWidgets(ttk.Frame):
self.tooltip_label.set(_("Enable Tooltips"))
def tooltips_toggle(self):
"""Toggles tooltips on/off and updates the menu label"""
"""
Toggles the visibility of tooltips (on/off) and updates
the corresponding menu label. Inverts the current tooltip state
(`self.tooltip_state`), saves the new value in the configuration,
and applies the change immediately. Updates the menu entry's label to
reflect the new tooltip status (e.g., "Tooltips: On" or "Tooltips: Off").
"""
# Toggle the boolean state
new_bool_state = not self.tooltip_state.get()
# Save the converted value in the configuration
@ -553,8 +547,8 @@ class FrameWidgets(ttk.Frame):
# This assumes it's the third item (index 2) in your menu
self.settings.entryconfigure(1, label=self.tooltip_label.get())
def update_theme_label(self) -> str:
"""Update the theme label based on current theme"""
def update_theme_label(self) -> None:
"""Update the theme label based on the current theme"""
current_theme = ConfigManager.get("theme")
if current_theme == "light":
self.theme_label.set(_("Dark"))
@ -583,8 +577,7 @@ class FrameWidgets(ttk.Frame):
)
self.btn_stst.grid(column=0, row=0, padx=5, pady=8)
tl = LxTools.get_file_name(AppConfig.TEMP_DIR)
if len(self.tl) == 0:
if self.l_box.size() == 0:
Tooltip(self.btn_stst, Msg.TTIP["empty_list"], self.tooltip_state)
else:
Tooltip(self.btn_stst, Msg.TTIP["start_tl"], self.tooltip_state)
@ -659,7 +652,6 @@ class FrameWidgets(ttk.Frame):
Msg.STR["invalid_base64"],
)
else:
print("Key is valid and does not exist import allowed!")
filepath = Path(filepath)
# Shorten the tunnel name to the maximum allowed length if it exceeds 12 characters.
original_name = filepath.name
@ -675,7 +667,7 @@ class FrameWidgets(ttk.Frame):
self.tl.update(data_import)
if self.a != "":
process: CompletedProcess[str] = subprocess.run(
process: CompletedProcess[str] = run(
["nmcli", "connection", "down", self.a],
capture_output=True,
text=True,
@ -683,13 +675,11 @@ class FrameWidgets(ttk.Frame):
)
if process.stderr:
print(process.stderr)
logging.error(f"{process.stderr}: Code {process.returncode}")
else:
print(f"Error process decrypt: Code {process.returncode}")
self.reset_fields()
process: CompletedProcess[str] = subprocess.run(
process: CompletedProcess[str] = run(
[
"nmcli",
"connection",
@ -705,18 +695,15 @@ class FrameWidgets(ttk.Frame):
)
if process.stderr:
print(process.stderr)
logging.error(
f"{process.stderr} Code: {process.returncode}", exc_info=True
)
if process.returncode == 0:
print(f"Tunnel >> {import_file.stem} << import successfull")
else:
print(f"Error process decrypt: Code {process.returncode}")
CryptoUtil.encrypt()
CryptoUtil.encrypt(getpass.getuser())
LxTools.clean_files(AppConfig.TEMP_DIR, file=None)
AppConfig.ensure_directories()
self.str_var.set("")
self.a = Tunnel.active()
self.a = Tunnel.get_active()
self.l_box.insert(0, self.a)
self.wg_autostart.configure(state="normal")
self.l_box.selection_clear(0, tk.END)
@ -741,7 +728,7 @@ class FrameWidgets(ttk.Frame):
self.stop()
self.handle_tunnel_data(self.a, self.tl)
self.show_data()
process: CompletedProcess[str] = subprocess.run(
process: CompletedProcess[str] = run(
["nmcli", "con", "mod", self.a, "connection.autoconnect", "no"],
capture_output=True,
text=True,
@ -749,7 +736,7 @@ class FrameWidgets(ttk.Frame):
)
if process.stderr:
print(process.stderr)
logging.error(process.stderr, exc_info=True)
if process.returncode == 0:
print(f">> {import_file.stem} << autostart is disabled by default")
@ -761,16 +748,10 @@ class FrameWidgets(ttk.Frame):
Msg.STR["imp_err"],
Msg.STR["no_valid_file"],
)
except IsADirectoryError:
except (IsADirectoryError, TypeError, FileNotFoundError):
print("File import: abort by user...")
except EOFError as e:
print(e)
except TypeError:
print("File import: abort by user...")
except FileNotFoundError:
print("File import: abort by user...")
except subprocess.CalledProcessError:
print("Tunnel exist!")
def delete(self) -> None:
"""
@ -780,7 +761,7 @@ class FrameWidgets(ttk.Frame):
self.select_tunnel = self.l_box.curselection()
select_tl = self.l_box.get(self.select_tunnel[0])
process: CompletedProcess[str] = subprocess.run(
process: CompletedProcess[str] = run(
["nmcli", "connection", "delete", select_tl],
capture_output=True,
text=True,
@ -788,12 +769,9 @@ class FrameWidgets(ttk.Frame):
)
if process.stderr:
print(process.stderr)
if process.returncode == 0:
print(f"Tunnel >> {select_tl} << successfully deleted...")
else:
print(f"Error process: Code {process.returncode}")
logging.error(
f"{process.stderr} Code: {process.returncode}", exc_info=True
)
self.l_box.delete(self.select_tunnel[0])
Path.unlink(f"{AppConfig.CONFIG_DIR}/{select_tl}.dat")
@ -867,9 +845,6 @@ class FrameWidgets(ttk.Frame):
"""
if ConfigManager.get("autostart") != "off":
print(
f"{ConfigManager.get("autostart")} starts automatically when the system starts."
)
self.selected_option.set(1)
self.autoconnect_var.set("")
self.auto_con = ConfigManager.get("autostart")
@ -908,9 +883,7 @@ class FrameWidgets(ttk.Frame):
if self.selected_option.get() == 0:
ConfigManager.set("autostart", "off")
tl = [f"{file}" for file in AppConfig.CONFIG_DIR.glob("*.dat")]
if len(tl) == 0:
if self.l_box.size() == 0:
self.wg_autostart.configure(state="disabled")
if self.selected_option.get() >= 1:
@ -923,9 +896,12 @@ class FrameWidgets(ttk.Frame):
def tl_rename(self) -> None:
"""
method to rename a tunnel
Method to rename a tunnel. Validates input for length,
special characters, and duplicate names,
performs the renaming via `nmcli` if valid, updates
the configuration file in the directory,
and adjusts UI elements such as listboxes and labels.
"""
name_of_file = LxTools.get_file_name(AppConfig.TEMP_DIR)
special_characters = ["\\", "/", "{", "}", " "]
if len(self.lb_rename.get()) > 12:
@ -955,7 +931,9 @@ class FrameWidgets(ttk.Frame):
Msg.STR["false_signs"],
)
elif self.lb_rename.get() in name_of_file:
elif self.lb_rename.get() in [
file.stem for file in AppConfig.CONFIG_DIR.glob("*.dat")
]:
LxTools.msg_window(
AppConfig.IMAGE_PATHS["icon_info"],
@ -971,7 +949,7 @@ class FrameWidgets(ttk.Frame):
select_tl = self.l_box.get(self.select_tunnel[0])
# nmcli connection modify old connection.id iphone
process: CompletedProcess[str] = subprocess.run(
process: CompletedProcess[str] = run(
[
"nmcli",
"connection",
@ -985,10 +963,9 @@ class FrameWidgets(ttk.Frame):
check=False,
)
if process.stderr:
print(process.stderr)
if process.returncode != 0:
print(f"Error process: Code {process.returncode}")
logging.error(
f"{process.stderr} Code: {process.returncode}", exc_info=True
)
source = Path(f"{AppConfig.CONFIG_DIR}/{select_tl}.dat")
destination = AppConfig.CONFIG_DIR / f"{self.lb_rename.get()}.dat"
@ -1012,14 +989,13 @@ class FrameWidgets(ttk.Frame):
Msg.STR["sel_list"],
)
except subprocess.CalledProcessError:
pass
except EOFError as e:
print(e)
logging.error(e, exc_info=True)
def handle_tunnel_data(self, active=None, data=None) -> None:
"""Processes tunnel data from an active connection and updates
UI elements like labels with information about address, DNS, and endpoint.
"""
tunnel = active
values = data[tunnel]
# Address Label
@ -1032,7 +1008,13 @@ class FrameWidgets(ttk.Frame):
def show_data(self) -> None:
"""
shows data in the label
Displays network-related data (address, DNS, endpoint)
in the UI using ttk.Label widgets.
Creates three labels for address, DNS, and endpoint with
specific styling (color, font), positioning them in a
grid layout (`lb_frame` and `lb_frame2`).
Each label is linked to a corresponding text variable
(`self.add`, `self.DNS`, `self.enp`) for dynamic data updates.
"""
# Address Label
self.address = ttk.Label(
@ -1055,7 +1037,13 @@ class FrameWidgets(ttk.Frame):
def wg_switch(self, event=None) -> None:
"""
Deals with switching the VPN connection
Manages switching between active and inactiveVPN connections.
If no tunnel is selected (`self.a == ""`), it starts a new connection
with the selected tunnel from the listbox (`l_box`).
Otherwise, it stops the current connection and updates
tunnel data using `handle_tunnel_data`.
Handles errors like `IndexError` by displaying appropriate
messages if no items are selected or the listbox is empty.
"""
try:
if self.a == "":
@ -1098,7 +1086,7 @@ class FrameWidgets(ttk.Frame):
"""
if action == "stop":
if self.a:
process: CompletedProcess[str] = subprocess.run(
process: CompletedProcess[str] = run(
["nmcli", "connection", "down", self.a],
capture_output=True,
text=True,
@ -1106,10 +1094,9 @@ class FrameWidgets(ttk.Frame):
)
if process.stderr:
print(process.stderr)
if process.returncode != 0:
print(f"Error process: Code {process.returncode}")
logging.error(
f"{process.stderr} Code: {process.returncode}", exc_info=True
)
self.update_connection_display()
self.reset_fields()
@ -1118,7 +1105,7 @@ class FrameWidgets(ttk.Frame):
elif action == "start":
if tunnel_name or self.a:
target_tunnel = tunnel_name or self.a
process: CompletedProcess[str] = subprocess.run(
process: CompletedProcess[str] = run(
["nmcli", "connection", "up", target_tunnel],
capture_output=True,
text=True,
@ -1126,12 +1113,9 @@ class FrameWidgets(ttk.Frame):
)
if process.stderr:
print(process.stderr)
if process.returncode == 0:
print(f"Tunnel >> {target_tunnel} << started")
else:
print(f"Error process: Code {process.returncode}")
logging.error(
f"{process.stderr} Code: {process.returncode}", exc_info=True
)
self.update_connection_display()
self.handle_tunnel_data(self.a, self.tl)
@ -1149,7 +1133,7 @@ class FrameWidgets(ttk.Frame):
"""
Updated the display after connection changes
"""
self.a = Tunnel.active()
self.a = Tunnel.get_active()
if not hasattr(self, "str_var"):
self.str_var = tk.StringVar()
self.str_var.set(self.a)
@ -1158,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, AppConfig.USER_FILE)
LxTools.sigi(AppConfig.TEMP_DIR)
window = Wirepy()
LogConfig.logger(ConfigManager.get("logfile"))
"""
the hidden files are hidden in Filedialog
"""
@ -1173,5 +1161,5 @@ if __name__ == "__main__":
window.tk.call("set", "::tk::dialog::file::showHiddenVar", "0")
window.mainloop()
LxTools.clean_files(AppConfig.TEMP_DIR, AppConfig.USER_FILE)
LxTools.clean_files(AppConfig.TEMP_DIR)
sys.exit(0)

69
wp_app_config.py Normal file → Executable file
View File

@ -1,15 +1,37 @@
#!/usr/bin/python3
"""App configuration for Wire-Py"""
import logging
import gettext
import locale
from pathlib import Path
import subprocess
from subprocess import CompletedProcess, run
from typing import Dict, Any
class AppConfig:
"""Central configuration class for Wire-Py application"""
"""Central configuration and system setup manager for the Wire-Py application.
This class serves as a singleton-like container for all global configuration data,
including paths, UI settings, localization, versioning, and system-specific resources.
It ensures that required directories, files, and services are created and configured
before the application starts. Additionally, it provides tools for managing translations,
default settings, and autostart functionality to maintain a consistent user experience.
Key Responsibilities:
- Centralizes all configuration values (paths, UI preferences, localization).
- Ensures required directories and files exist on startup.
- Handles translation setup via `gettext` for multilingual support.
- Manages default settings file generation.
- Configures autostart services using systemd for user-specific launch behavior.
This class is used globally across the application to access configuration data
consistently and perform system-level setup tasks.
"""
# Logging
LOG_DIR = Path.home() / ".local/share/lxlogs"
Path(LOG_DIR).mkdir(parents=True, exist_ok=True)
LOG_FILE_PATH = LOG_DIR / "wirepy.log"
# Localization
APP_NAME: str = "wirepy"
@ -19,7 +41,6 @@ class AppConfig:
BASE_DIR: Path = Path.home()
CONFIG_DIR: Path = BASE_DIR / ".config/wire_py"
TEMP_DIR: Path = Path("/tmp/tlecdcwg")
USER_FILE: Path = Path("/tmp/.log_user")
PUBLICKEY: Path = CONFIG_DIR / "pbwgk.pem"
# Configuration files
@ -31,6 +52,7 @@ class AppConfig:
"# Theme": "dark",
"# Tooltips": True,
"# Autostart": "off",
"# Logfile": LOG_FILE_PATH,
}
# Updates
@ -42,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,
@ -49,7 +72,7 @@ class AppConfig:
}
# System-dependent paths
SYSTEM_PATHS: Dict[str, str] = {
SYSTEM_PATHS: Dict[str, Path] = {
"ssl_decrypt": "/usr/local/bin/ssl_decrypt.py",
"ssl_encrypt": "/usr/local/bin/ssl_encrypt.py",
"tcl_path": "/usr/share/TK-Themes",
@ -57,7 +80,7 @@ class AppConfig:
}
# Images and icons paths
IMAGE_PATHS: Dict[str, str] = {
IMAGE_PATHS: Dict[str, Path] = {
"icon_vpn": "/usr/share/icons/lx-icons/48/wg_vpn.png",
"icon_msg": "/usr/share/icons/lx-icons/48/wg_msg.png",
"icon_import": "/usr/share/icons/lx-icons/48/wg_import.png",
@ -67,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
@ -100,9 +124,9 @@ class AppConfig:
cls.SETTINGS_FILE.write_text(content)
@classmethod
def get_autostart_content(cls) -> str:
"""Returns the content for the autostart service file"""
SYSTEMD_FILE: list[str] = [
def get_autostart_content(cls) -> None:
"""Returns the content for an autostart service file"""
systemd_file: list[str] = [
"[Unit]",
"Description=Automatic Tunnel Start",
"After=network-online.target",
@ -118,27 +142,29 @@ class AppConfig:
if not cls.SYSTEMD_USER_FOLDER.exists():
cls.SYSTEMD_USER_FOLDER.mkdir(parents=True, exist_ok=True)
from subprocess import CompletedProcess
if not cls.AUTOSTART_SERVICE.is_file():
content = "\n".join([line for line in SYSTEMD_FILE])
content = "\n".join([line for line in systemd_file])
cls.AUTOSTART_SERVICE.write_text(content)
process: CompletedProcess[str] = subprocess.run(
process: CompletedProcess[str] = run(
["systemctl", "--user", "enable", "wg_start.service"],
capture_output=True,
text=True,
check=False,
)
print(process.stdout)
if process.returncode == 0:
print(process.stdout)
else:
print(f"Error with the following code... {process.returncode}")
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 inizialize the class for translate strrings
# here is initializing the class for translation strings
_ = AppConfig.setup_translations()
@ -172,6 +198,11 @@ class Msg:
"imp_err": _("Import Error"),
"exp_err": _("Export Error"),
"exp_try": _("Export failed! Please try again"),
"exp_zip": _(
"The error occurs because the zipfile module encountered an issue.\n"
"Please verify that you have the latest version of WirePy installed.\n"
"You can also contact the WirePy developer team to resolve this issue quickly.\n"
),
"tl_first": _("Please first import tunnel"),
"sel_list": _("Please select a tunnel from the list"),
"sign_len": _("The new name may contain only 12 characters"),