Compare commits
7 Commits
55f2119bc3
...
f682858051
Author | SHA1 | Date | |
---|---|---|---|
f682858051 | |||
68580d0ded | |||
b764547d16 | |||
7f4fabe856 | |||
79f6fc0265 | |||
5ac37ad9ad | |||
4cdcfadbac |
11
Changelog
@ -7,23 +7,26 @@ My standard System: Linux Mint 22 Cinnamon
|
|||||||
- If Wire-Py already runs, prevent further start
|
- If Wire-Py already runs, prevent further start
|
||||||
- for loops with lists replaced by List Comprehensions
|
- for loops with lists replaced by List Comprehensions
|
||||||
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
13-04-0725
|
03-06-2025
|
||||||
|
|
||||||
|
-
|
||||||
|
### Added
|
||||||
|
13-04-20255
|
||||||
|
|
||||||
- Installer update for Open Suse Tumbleweed and Leap
|
- Installer update for Open Suse Tumbleweed and Leap
|
||||||
- add symbolic link wirepy.py
|
- add symbolic link wirepy.py
|
||||||
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
09-04-0725
|
09-04-2025
|
||||||
|
|
||||||
- Installer now with query and remove
|
- Installer now with query and remove
|
||||||
- Icons merged
|
- Icons merged
|
||||||
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
07-04-0725
|
07-04-2025
|
||||||
|
|
||||||
- Installers will support other systems again
|
- Installers will support other systems again
|
||||||
- Installer is now finished clean with wrong password
|
- Installer is now finished clean with wrong password
|
||||||
|
865
common_tools.py
@ -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
@ -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 |
@ -42,13 +42,13 @@ def search_string_in_directory(
|
|||||||
except Exception:
|
except Exception:
|
||||||
continue # Skip files that cause errors
|
continue # Skip files that cause errors
|
||||||
|
|
||||||
# Invert the logic: return False if string is found, True otherwise
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def main() -> bool:
|
def main() -> None:
|
||||||
parser = argparse.ArgumentParser(
|
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")
|
parser.add_argument("search_string", help="Search string")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
@ -1,19 +1,30 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
""" This Script decrypt Wireguard files for Wirepy users """
|
""" This Script decrypt Wireguard files for Wirepy users """
|
||||||
|
import argparse
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import pwd
|
||||||
import shutil
|
import shutil
|
||||||
from subprocess import CompletedProcess
|
from subprocess import CompletedProcess, run
|
||||||
import subprocess
|
from shared_libs.wp_app_config import AppConfig, logging
|
||||||
from wp_app_config import AppConfig
|
|
||||||
|
|
||||||
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:
|
||||||
path_of_crypted_tunnel: Path = Path(f"/home/{log_name}/.config/wire_py")
|
# 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():
|
if not keyfile.is_file():
|
||||||
process: CompletedProcess[str] = subprocess.run(
|
process: CompletedProcess[str] = run(
|
||||||
[
|
[
|
||||||
"openssl",
|
"openssl",
|
||||||
"rsa",
|
"rsa",
|
||||||
@ -29,14 +40,16 @@ if not keyfile.is_file():
|
|||||||
text=True,
|
text=True,
|
||||||
check=False,
|
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")]
|
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
|
base_name = Path(tunnel_path).stem
|
||||||
|
|
||||||
process: CompletedProcess[str] = subprocess.run(
|
process: CompletedProcess[str] = run(
|
||||||
[
|
[
|
||||||
"openssl",
|
"openssl",
|
||||||
"pkeyutl",
|
"pkeyutl",
|
||||||
@ -60,17 +73,12 @@ if AppConfig.PUBLICKEY.exists:
|
|||||||
text=True,
|
text=True,
|
||||||
check=False,
|
check=False,
|
||||||
)
|
)
|
||||||
shutil.chown(f"{AppConfig.TEMP_DIR}/{base_name}.conf", 1000, 1000)
|
shutil.chown(f"{AppConfig.TEMP_DIR}/{base_name}.conf", uid, gid)
|
||||||
print(f"Processing of the file: {tunnel_path}")
|
logging.info(f"Processing of the file: {tunnel_path}", exc_info=True)
|
||||||
|
|
||||||
if process.stdout:
|
|
||||||
print(process.stdout)
|
|
||||||
|
|
||||||
# Output from Openssl Error
|
# Output from Openssl Error
|
||||||
if process.stderr:
|
if process.stderr:
|
||||||
print("(Error):", process.stderr)
|
logging.error(
|
||||||
|
f"{process.stderr} Error by [{tunnel_path}] Code: {process.returncode}",
|
||||||
if process.returncode == 0:
|
exc_info=True,
|
||||||
print(f"File {base_name}.dat successfully decrypted.")
|
)
|
||||||
else:
|
|
||||||
print(f"Error by {tunnel_path}: Code: {process.returncode}")
|
|
||||||
|
@ -1,20 +1,33 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
""" This Script encrypt Wireguardfiles for Wirepy users for more Security """
|
""" This Script encrypt Wireguardfiles for Wirepy users for more Security """
|
||||||
|
|
||||||
|
import argparse
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import pwd
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
from subprocess import CompletedProcess, run
|
||||||
from subprocess import CompletedProcess
|
from shared_libs.wp_app_config import AppConfig, logging
|
||||||
from wp_app_config import AppConfig
|
|
||||||
|
|
||||||
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():
|
if not keyfile.is_file():
|
||||||
|
|
||||||
process: CompletedProcess[str] = subprocess.run(
|
process: CompletedProcess[str] = run(
|
||||||
[
|
[
|
||||||
"openssl",
|
"openssl",
|
||||||
"rsa",
|
"rsa",
|
||||||
@ -31,19 +44,14 @@ if not keyfile.is_file():
|
|||||||
check=False,
|
check=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
if process.stdout:
|
|
||||||
print(process.stdout)
|
|
||||||
|
|
||||||
# Output from Openssl Error
|
# Output from Openssl Error
|
||||||
if process.stderr:
|
if process.stderr:
|
||||||
print("(Error):", process.stderr)
|
logging.error(f"{process.stderr} Code: {process.returncode}", exc_info=True)
|
||||||
|
|
||||||
if process.returncode == 0:
|
if process.returncode == 0:
|
||||||
print("Public key generated successfully.")
|
logging.info("Public key generated successfully.", exc_info=True)
|
||||||
else:
|
|
||||||
print(f"Error generate Publickey: Code: {process.returncode}")
|
|
||||||
|
|
||||||
shutil.chown(keyfile, 1000, 1000)
|
shutil.chown(keyfile, uid, gid)
|
||||||
|
|
||||||
# any() get True when directory is not empty
|
# any() get True when directory is not empty
|
||||||
if AppConfig.TEMP_DIR.exists() and any(AppConfig.TEMP_DIR.iterdir()):
|
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:
|
for config_file in clear_files:
|
||||||
base_name = Path(config_file).stem
|
base_name = Path(config_file).stem
|
||||||
process: CompletedProcess[str] = subprocess.run(
|
process: CompletedProcess[str] = run(
|
||||||
[
|
[
|
||||||
"openssl",
|
"openssl",
|
||||||
"pkeyutl",
|
"pkeyutl",
|
||||||
@ -69,13 +77,6 @@ if AppConfig.TEMP_DIR.exists() and any(AppConfig.TEMP_DIR.iterdir()):
|
|||||||
check=False,
|
check=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
print(f"Processing of the file: {config_file}")
|
|
||||||
|
|
||||||
# Output from Openssl Error
|
# Output from Openssl Error
|
||||||
if process.stderr:
|
if process.stderr:
|
||||||
print("(Error):", process.stderr)
|
logging.error(process.stderr, exc_info=True)
|
||||||
|
|
||||||
if process.returncode == 0:
|
|
||||||
print(f"File {base_name}.dat successfully encrypted.")
|
|
||||||
else:
|
|
||||||
print(f"Error by {config_file}: Code: {process.returncode}")
|
|
||||||
|
15
start_wg.py
@ -2,16 +2,15 @@
|
|||||||
"""
|
"""
|
||||||
This script belongs to wirepy and is for the auto start of the tunnel
|
This script belongs to wirepy and is for the auto start of the tunnel
|
||||||
"""
|
"""
|
||||||
|
import logging
|
||||||
import subprocess
|
from subprocess import CompletedProcess, run
|
||||||
from subprocess import CompletedProcess
|
from shared_libs.wp_app_config import AppConfig
|
||||||
from wp_app_config import AppConfig
|
from shared_libs.common_tools import ConfigManager, LogConfig
|
||||||
from common_tools import ConfigManager
|
|
||||||
|
|
||||||
ConfigManager.init(AppConfig.SETTINGS_FILE)
|
ConfigManager.init(AppConfig.SETTINGS_FILE)
|
||||||
|
LogConfig.logger(ConfigManager.get("logfile"))
|
||||||
if ConfigManager.get("autostart") != "off":
|
if ConfigManager.get("autostart") != "off":
|
||||||
process: CompletedProcess[str] = subprocess.run(
|
process: CompletedProcess[str] = run(
|
||||||
["nmcli", "connection", "up", ConfigManager.get("autostart")],
|
["nmcli", "connection", "up", ConfigManager.get("autostart")],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
@ -19,7 +18,7 @@ if ConfigManager.get("autostart") != "off":
|
|||||||
)
|
)
|
||||||
# Output from start_wg error
|
# Output from start_wg error
|
||||||
if process.stderr:
|
if process.stderr:
|
||||||
print(process.stderr) # this is for the error, later on logfile
|
logging.error(process.stderr, exc_info=True)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
|
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
|
|
206
wirepy.py
@ -2,32 +2,28 @@
|
|||||||
"""
|
"""
|
||||||
this script is a simple GUI for managing Wireguard Tunnels
|
this script is a simple GUI for managing Wireguard Tunnels
|
||||||
"""
|
"""
|
||||||
|
import logging
|
||||||
import getpass
|
import getpass
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
import webbrowser
|
import webbrowser
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from subprocess import CompletedProcess
|
from subprocess import CompletedProcess, run
|
||||||
from tkinter import TclError, filedialog, ttk
|
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,
|
ConfigManager,
|
||||||
ThemeManager,
|
ThemeManager,
|
||||||
CryptoUtil,
|
|
||||||
GiteaUpdate,
|
|
||||||
Tunnel,
|
|
||||||
Tooltip,
|
Tooltip,
|
||||||
LxTools,
|
|
||||||
)
|
)
|
||||||
from wp_app_config import AppConfig, Msg
|
|
||||||
|
|
||||||
AppConfig.USER_FILE.write_text(getpass.getuser())
|
from shared_libs.wp_app_config import AppConfig, Msg
|
||||||
AppConfig.ensure_directories()
|
|
||||||
AppConfig.create_default_settings()
|
|
||||||
CryptoUtil.decrypt()
|
|
||||||
|
|
||||||
|
|
||||||
class Wirepy(tk.Tk):
|
class Wirepy(tk.Tk):
|
||||||
@ -171,7 +167,11 @@ class FrameWidgets(ttk.Frame):
|
|||||||
self.settings.add_command(
|
self.settings.add_command(
|
||||||
label=self.theme_label.get(), command=self.on_theme_toggle
|
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
|
# About BTN Menu / Label
|
||||||
self.about_btn = ttk.Button(
|
self.about_btn = ttk.Button(
|
||||||
self.menu_frame, text=_("About"), style="Toolbutton", command=self.about
|
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.about_btn.grid(column=2, columnspan=2, row=0)
|
||||||
self.readme = tk.Menu(self)
|
self.readme = tk.Menu(self)
|
||||||
|
|
||||||
self.a = Tunnel.active()
|
self.a = Tunnel.get_active()
|
||||||
|
|
||||||
# Label Frame 1
|
# Label Frame 1
|
||||||
self.lb_frame_btn_lbox = ttk.Frame(self)
|
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)
|
self.tl = Tunnel.parse_files_to_dictionary(directory=AppConfig.TEMP_DIR)
|
||||||
LxTools.clean_files(AppConfig.TEMP_DIR, file=None)
|
LxTools.clean_files(AppConfig.TEMP_DIR, file=None)
|
||||||
AppConfig.ensure_directories()
|
AppConfig.ensure_directories()
|
||||||
# self.tl = LxTools.get_file_name(AppConfig.TEMP_DIR)
|
|
||||||
for tunnels, values in self.tl.items():
|
for tunnels, values in self.tl.items():
|
||||||
self.l_box.insert("end", tunnels)
|
self.l_box.insert("end", tunnels)
|
||||||
self.l_box.update()
|
self.l_box.update()
|
||||||
@ -294,14 +294,7 @@ class FrameWidgets(ttk.Frame):
|
|||||||
self.btn_exp = ttk.Button(
|
self.btn_exp = ttk.Button(
|
||||||
self.lb_frame_btn_lbox,
|
self.lb_frame_btn_lbox,
|
||||||
image=self.exp_pic,
|
image=self.exp_pic,
|
||||||
command=lambda: Tunnel.export(
|
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"],
|
|
||||||
),
|
|
||||||
padding=0,
|
padding=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -409,7 +402,7 @@ class FrameWidgets(ttk.Frame):
|
|||||||
|
|
||||||
# Update the labels based on the result
|
# Update the labels based on the result
|
||||||
def update_ui_for_update(self, res):
|
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
|
# First, remove the update button if it exists to avoid conflicts
|
||||||
if hasattr(self, "update_btn"):
|
if hasattr(self, "update_btn"):
|
||||||
self.update_btn.grid_forget()
|
self.update_btn.grid_forget()
|
||||||
@ -421,13 +414,13 @@ class FrameWidgets(ttk.Frame):
|
|||||||
self.update_tooltip.set(_("Updates you have disabled"))
|
self.update_tooltip.set(_("Updates you have disabled"))
|
||||||
# Clear the foreground color as requested
|
# Clear the foreground color as requested
|
||||||
self.update_foreground.set("")
|
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)
|
Tooltip(self.updates_lb, self.update_tooltip.get(), self.tooltip_state)
|
||||||
|
|
||||||
elif res == "No Internet Connection!":
|
elif res == "No Internet Connection!":
|
||||||
self.update_label.set(_("No Server Connection!"))
|
self.update_label.set(_("No Server Connection!"))
|
||||||
self.update_foreground.set("red")
|
self.update_foreground.set("red")
|
||||||
# Set tooltip for "No Server Connection"
|
# Set the tooltip for "No Server Connection"
|
||||||
Tooltip(
|
Tooltip(
|
||||||
self.updates_lb,
|
self.updates_lb,
|
||||||
_("Could not connect to update server"),
|
_("Could not connect to update server"),
|
||||||
@ -438,7 +431,7 @@ class FrameWidgets(ttk.Frame):
|
|||||||
self.update_label.set(_("No Updates"))
|
self.update_label.set(_("No Updates"))
|
||||||
self.update_tooltip.set(_("Congratulations! Wire-Py is up to date"))
|
self.update_tooltip.set(_("Congratulations! Wire-Py is up to date"))
|
||||||
self.update_foreground.set("")
|
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)
|
Tooltip(self.updates_lb, self.update_tooltip.get(), self.tooltip_state)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -460,12 +453,7 @@ class FrameWidgets(ttk.Frame):
|
|||||||
self.download.add_command(
|
self.download.add_command(
|
||||||
label=_("Download"),
|
label=_("Download"),
|
||||||
command=lambda: GiteaUpdate.download(
|
command=lambda: GiteaUpdate.download(
|
||||||
f"{AppConfig.DOWNLOAD_URL}/{res}.zip",
|
f"{AppConfig.DOWNLOAD_URL}/{res}.zip", res
|
||||||
res,
|
|
||||||
AppConfig.IMAGE_PATHS["icon_info"],
|
|
||||||
AppConfig.IMAGE_PATHS["icon_vpn"],
|
|
||||||
AppConfig.IMAGE_PATHS["icon_error"],
|
|
||||||
AppConfig.IMAGE_PATHS["icon_msg"],
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -475,7 +463,7 @@ class FrameWidgets(ttk.Frame):
|
|||||||
a tk.Toplevel window
|
a tk.Toplevel window
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def link_btn() -> str | None:
|
def link_btn() -> None:
|
||||||
webbrowser.open("https://git.ilunix.de/punix/Wire-Py")
|
webbrowser.open("https://git.ilunix.de/punix/Wire-Py")
|
||||||
|
|
||||||
msg_t = _(
|
msg_t = _(
|
||||||
@ -514,7 +502,7 @@ class FrameWidgets(ttk.Frame):
|
|||||||
AppConfig.UPDATE_URL, AppConfig.VERSION, "on"
|
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"):
|
if hasattr(self, "update_btn"):
|
||||||
self.update_btn.grid_forget()
|
self.update_btn.grid_forget()
|
||||||
if hasattr(self, "updates_lb"):
|
if hasattr(self, "updates_lb"):
|
||||||
@ -523,7 +511,7 @@ class FrameWidgets(ttk.Frame):
|
|||||||
# Now update the UI with the fresh result
|
# Now update the UI with the fresh result
|
||||||
self.update_ui_for_update(res)
|
self.update_ui_for_update(res)
|
||||||
except Exception as e:
|
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
|
# Fallback to a default message if there's an error
|
||||||
self.update_ui_for_update("No Internet Connection!")
|
self.update_ui_for_update("No Internet Connection!")
|
||||||
|
|
||||||
@ -538,7 +526,13 @@ class FrameWidgets(ttk.Frame):
|
|||||||
self.tooltip_label.set(_("Enable Tooltips"))
|
self.tooltip_label.set(_("Enable Tooltips"))
|
||||||
|
|
||||||
def tooltips_toggle(self):
|
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
|
# Toggle the boolean state
|
||||||
new_bool_state = not self.tooltip_state.get()
|
new_bool_state = not self.tooltip_state.get()
|
||||||
# Save the converted value in the configuration
|
# 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
|
# This assumes it's the third item (index 2) in your menu
|
||||||
self.settings.entryconfigure(1, label=self.tooltip_label.get())
|
self.settings.entryconfigure(1, label=self.tooltip_label.get())
|
||||||
|
|
||||||
def update_theme_label(self) -> str:
|
def update_theme_label(self) -> None:
|
||||||
"""Update the theme label based on current theme"""
|
"""Update the theme label based on the current theme"""
|
||||||
current_theme = ConfigManager.get("theme")
|
current_theme = ConfigManager.get("theme")
|
||||||
if current_theme == "light":
|
if current_theme == "light":
|
||||||
self.theme_label.set(_("Dark"))
|
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)
|
self.btn_stst.grid(column=0, row=0, padx=5, pady=8)
|
||||||
|
|
||||||
tl = LxTools.get_file_name(AppConfig.TEMP_DIR)
|
if self.l_box.size() == 0:
|
||||||
if len(self.tl) == 0:
|
|
||||||
Tooltip(self.btn_stst, Msg.TTIP["empty_list"], self.tooltip_state)
|
Tooltip(self.btn_stst, Msg.TTIP["empty_list"], self.tooltip_state)
|
||||||
else:
|
else:
|
||||||
Tooltip(self.btn_stst, Msg.TTIP["start_tl"], self.tooltip_state)
|
Tooltip(self.btn_stst, Msg.TTIP["start_tl"], self.tooltip_state)
|
||||||
@ -659,7 +652,6 @@ class FrameWidgets(ttk.Frame):
|
|||||||
Msg.STR["invalid_base64"],
|
Msg.STR["invalid_base64"],
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
print("Key is valid and does not exist – import allowed!")
|
|
||||||
filepath = Path(filepath)
|
filepath = Path(filepath)
|
||||||
# Shorten the tunnel name to the maximum allowed length if it exceeds 12 characters.
|
# Shorten the tunnel name to the maximum allowed length if it exceeds 12 characters.
|
||||||
original_name = filepath.name
|
original_name = filepath.name
|
||||||
@ -675,7 +667,7 @@ class FrameWidgets(ttk.Frame):
|
|||||||
self.tl.update(data_import)
|
self.tl.update(data_import)
|
||||||
|
|
||||||
if self.a != "":
|
if self.a != "":
|
||||||
process: CompletedProcess[str] = subprocess.run(
|
process: CompletedProcess[str] = run(
|
||||||
["nmcli", "connection", "down", self.a],
|
["nmcli", "connection", "down", self.a],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
@ -683,13 +675,11 @@ class FrameWidgets(ttk.Frame):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if process.stderr:
|
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()
|
self.reset_fields()
|
||||||
|
|
||||||
process: CompletedProcess[str] = subprocess.run(
|
process: CompletedProcess[str] = run(
|
||||||
[
|
[
|
||||||
"nmcli",
|
"nmcli",
|
||||||
"connection",
|
"connection",
|
||||||
@ -705,18 +695,15 @@ class FrameWidgets(ttk.Frame):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if process.stderr:
|
if process.stderr:
|
||||||
print(process.stderr)
|
logging.error(
|
||||||
|
f"{process.stderr} Code: {process.returncode}", exc_info=True
|
||||||
|
)
|
||||||
|
|
||||||
if process.returncode == 0:
|
CryptoUtil.encrypt(getpass.getuser())
|
||||||
print(f"Tunnel >> {import_file.stem} << import successfull")
|
|
||||||
else:
|
|
||||||
print(f"Error process decrypt: Code {process.returncode}")
|
|
||||||
|
|
||||||
CryptoUtil.encrypt()
|
|
||||||
LxTools.clean_files(AppConfig.TEMP_DIR, file=None)
|
LxTools.clean_files(AppConfig.TEMP_DIR, file=None)
|
||||||
AppConfig.ensure_directories()
|
AppConfig.ensure_directories()
|
||||||
self.str_var.set("")
|
self.str_var.set("")
|
||||||
self.a = Tunnel.active()
|
self.a = Tunnel.get_active()
|
||||||
self.l_box.insert(0, self.a)
|
self.l_box.insert(0, self.a)
|
||||||
self.wg_autostart.configure(state="normal")
|
self.wg_autostart.configure(state="normal")
|
||||||
self.l_box.selection_clear(0, tk.END)
|
self.l_box.selection_clear(0, tk.END)
|
||||||
@ -741,7 +728,7 @@ class FrameWidgets(ttk.Frame):
|
|||||||
self.stop()
|
self.stop()
|
||||||
self.handle_tunnel_data(self.a, self.tl)
|
self.handle_tunnel_data(self.a, self.tl)
|
||||||
self.show_data()
|
self.show_data()
|
||||||
process: CompletedProcess[str] = subprocess.run(
|
process: CompletedProcess[str] = run(
|
||||||
["nmcli", "con", "mod", self.a, "connection.autoconnect", "no"],
|
["nmcli", "con", "mod", self.a, "connection.autoconnect", "no"],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
@ -749,7 +736,7 @@ class FrameWidgets(ttk.Frame):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if process.stderr:
|
if process.stderr:
|
||||||
print(process.stderr)
|
logging.error(process.stderr, exc_info=True)
|
||||||
|
|
||||||
if process.returncode == 0:
|
if process.returncode == 0:
|
||||||
print(f">> {import_file.stem} << autostart is disabled by default")
|
print(f">> {import_file.stem} << autostart is disabled by default")
|
||||||
@ -761,16 +748,10 @@ class FrameWidgets(ttk.Frame):
|
|||||||
Msg.STR["imp_err"],
|
Msg.STR["imp_err"],
|
||||||
Msg.STR["no_valid_file"],
|
Msg.STR["no_valid_file"],
|
||||||
)
|
)
|
||||||
except IsADirectoryError:
|
except (IsADirectoryError, TypeError, FileNotFoundError):
|
||||||
print("File import: abort by user...")
|
print("File import: abort by user...")
|
||||||
except EOFError as e:
|
except EOFError as e:
|
||||||
print(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:
|
def delete(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -780,7 +761,7 @@ class FrameWidgets(ttk.Frame):
|
|||||||
self.select_tunnel = self.l_box.curselection()
|
self.select_tunnel = self.l_box.curselection()
|
||||||
select_tl = self.l_box.get(self.select_tunnel[0])
|
select_tl = self.l_box.get(self.select_tunnel[0])
|
||||||
|
|
||||||
process: CompletedProcess[str] = subprocess.run(
|
process: CompletedProcess[str] = run(
|
||||||
["nmcli", "connection", "delete", select_tl],
|
["nmcli", "connection", "delete", select_tl],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
@ -788,12 +769,9 @@ class FrameWidgets(ttk.Frame):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if process.stderr:
|
if process.stderr:
|
||||||
print(process.stderr)
|
logging.error(
|
||||||
|
f"{process.stderr} Code: {process.returncode}", exc_info=True
|
||||||
if process.returncode == 0:
|
)
|
||||||
print(f"Tunnel >> {select_tl} << successfully deleted...")
|
|
||||||
else:
|
|
||||||
print(f"Error process: Code {process.returncode}")
|
|
||||||
|
|
||||||
self.l_box.delete(self.select_tunnel[0])
|
self.l_box.delete(self.select_tunnel[0])
|
||||||
Path.unlink(f"{AppConfig.CONFIG_DIR}/{select_tl}.dat")
|
Path.unlink(f"{AppConfig.CONFIG_DIR}/{select_tl}.dat")
|
||||||
@ -867,9 +845,6 @@ class FrameWidgets(ttk.Frame):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if ConfigManager.get("autostart") != "off":
|
if ConfigManager.get("autostart") != "off":
|
||||||
print(
|
|
||||||
f"{ConfigManager.get("autostart")} starts automatically when the system starts."
|
|
||||||
)
|
|
||||||
self.selected_option.set(1)
|
self.selected_option.set(1)
|
||||||
self.autoconnect_var.set("")
|
self.autoconnect_var.set("")
|
||||||
self.auto_con = ConfigManager.get("autostart")
|
self.auto_con = ConfigManager.get("autostart")
|
||||||
@ -908,9 +883,7 @@ class FrameWidgets(ttk.Frame):
|
|||||||
if self.selected_option.get() == 0:
|
if self.selected_option.get() == 0:
|
||||||
ConfigManager.set("autostart", "off")
|
ConfigManager.set("autostart", "off")
|
||||||
|
|
||||||
tl = [f"{file}" for file in AppConfig.CONFIG_DIR.glob("*.dat")]
|
if self.l_box.size() == 0:
|
||||||
|
|
||||||
if len(tl) == 0:
|
|
||||||
self.wg_autostart.configure(state="disabled")
|
self.wg_autostart.configure(state="disabled")
|
||||||
|
|
||||||
if self.selected_option.get() >= 1:
|
if self.selected_option.get() >= 1:
|
||||||
@ -923,9 +896,12 @@ class FrameWidgets(ttk.Frame):
|
|||||||
|
|
||||||
def tl_rename(self) -> None:
|
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 = ["\\", "/", "{", "}", " "]
|
special_characters = ["\\", "/", "{", "}", " "]
|
||||||
|
|
||||||
if len(self.lb_rename.get()) > 12:
|
if len(self.lb_rename.get()) > 12:
|
||||||
@ -955,7 +931,9 @@ class FrameWidgets(ttk.Frame):
|
|||||||
Msg.STR["false_signs"],
|
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(
|
LxTools.msg_window(
|
||||||
AppConfig.IMAGE_PATHS["icon_info"],
|
AppConfig.IMAGE_PATHS["icon_info"],
|
||||||
@ -971,7 +949,7 @@ class FrameWidgets(ttk.Frame):
|
|||||||
select_tl = self.l_box.get(self.select_tunnel[0])
|
select_tl = self.l_box.get(self.select_tunnel[0])
|
||||||
|
|
||||||
# nmcli connection modify old connection.id iphone
|
# nmcli connection modify old connection.id iphone
|
||||||
process: CompletedProcess[str] = subprocess.run(
|
process: CompletedProcess[str] = run(
|
||||||
[
|
[
|
||||||
"nmcli",
|
"nmcli",
|
||||||
"connection",
|
"connection",
|
||||||
@ -985,10 +963,9 @@ class FrameWidgets(ttk.Frame):
|
|||||||
check=False,
|
check=False,
|
||||||
)
|
)
|
||||||
if process.stderr:
|
if process.stderr:
|
||||||
print(process.stderr)
|
logging.error(
|
||||||
|
f"{process.stderr} Code: {process.returncode}", exc_info=True
|
||||||
if process.returncode != 0:
|
)
|
||||||
print(f"Error process: Code {process.returncode}")
|
|
||||||
|
|
||||||
source = Path(f"{AppConfig.CONFIG_DIR}/{select_tl}.dat")
|
source = Path(f"{AppConfig.CONFIG_DIR}/{select_tl}.dat")
|
||||||
destination = AppConfig.CONFIG_DIR / f"{self.lb_rename.get()}.dat"
|
destination = AppConfig.CONFIG_DIR / f"{self.lb_rename.get()}.dat"
|
||||||
@ -1012,14 +989,13 @@ class FrameWidgets(ttk.Frame):
|
|||||||
Msg.STR["sel_list"],
|
Msg.STR["sel_list"],
|
||||||
)
|
)
|
||||||
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
except EOFError as e:
|
except EOFError as e:
|
||||||
print(e)
|
logging.error(e, exc_info=True)
|
||||||
|
|
||||||
def handle_tunnel_data(self, active=None, data=None) -> None:
|
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
|
tunnel = active
|
||||||
values = data[tunnel]
|
values = data[tunnel]
|
||||||
# Address Label
|
# Address Label
|
||||||
@ -1032,7 +1008,13 @@ class FrameWidgets(ttk.Frame):
|
|||||||
|
|
||||||
def show_data(self) -> None:
|
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
|
# Address Label
|
||||||
self.address = ttk.Label(
|
self.address = ttk.Label(
|
||||||
@ -1055,7 +1037,13 @@ class FrameWidgets(ttk.Frame):
|
|||||||
|
|
||||||
def wg_switch(self, event=None) -> None:
|
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:
|
try:
|
||||||
if self.a == "":
|
if self.a == "":
|
||||||
@ -1098,7 +1086,7 @@ class FrameWidgets(ttk.Frame):
|
|||||||
"""
|
"""
|
||||||
if action == "stop":
|
if action == "stop":
|
||||||
if self.a:
|
if self.a:
|
||||||
process: CompletedProcess[str] = subprocess.run(
|
process: CompletedProcess[str] = run(
|
||||||
["nmcli", "connection", "down", self.a],
|
["nmcli", "connection", "down", self.a],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
@ -1106,10 +1094,9 @@ class FrameWidgets(ttk.Frame):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if process.stderr:
|
if process.stderr:
|
||||||
print(process.stderr)
|
logging.error(
|
||||||
|
f"{process.stderr} Code: {process.returncode}", exc_info=True
|
||||||
if process.returncode != 0:
|
)
|
||||||
print(f"Error process: Code {process.returncode}")
|
|
||||||
|
|
||||||
self.update_connection_display()
|
self.update_connection_display()
|
||||||
self.reset_fields()
|
self.reset_fields()
|
||||||
@ -1118,7 +1105,7 @@ class FrameWidgets(ttk.Frame):
|
|||||||
elif action == "start":
|
elif action == "start":
|
||||||
if tunnel_name or self.a:
|
if tunnel_name or self.a:
|
||||||
target_tunnel = 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],
|
["nmcli", "connection", "up", target_tunnel],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
@ -1126,12 +1113,9 @@ class FrameWidgets(ttk.Frame):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if process.stderr:
|
if process.stderr:
|
||||||
print(process.stderr)
|
logging.error(
|
||||||
|
f"{process.stderr} Code: {process.returncode}", exc_info=True
|
||||||
if process.returncode == 0:
|
)
|
||||||
print(f"Tunnel >> {target_tunnel} << started")
|
|
||||||
else:
|
|
||||||
print(f"Error process: Code {process.returncode}")
|
|
||||||
|
|
||||||
self.update_connection_display()
|
self.update_connection_display()
|
||||||
self.handle_tunnel_data(self.a, self.tl)
|
self.handle_tunnel_data(self.a, self.tl)
|
||||||
@ -1149,7 +1133,7 @@ class FrameWidgets(ttk.Frame):
|
|||||||
"""
|
"""
|
||||||
Updated the display after connection changes
|
Updated the display after connection changes
|
||||||
"""
|
"""
|
||||||
self.a = Tunnel.active()
|
self.a = Tunnel.get_active()
|
||||||
if not hasattr(self, "str_var"):
|
if not hasattr(self, "str_var"):
|
||||||
self.str_var = tk.StringVar()
|
self.str_var = tk.StringVar()
|
||||||
self.str_var.set(self.a)
|
self.str_var.set(self.a)
|
||||||
@ -1158,10 +1142,14 @@ class FrameWidgets(ttk.Frame):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
AppConfig.ensure_directories()
|
||||||
|
AppConfig.create_default_settings()
|
||||||
|
CryptoUtil.decrypt(getpass.getuser(), AppConfig.CONFIG_DIR)
|
||||||
_ = AppConfig.setup_translations()
|
_ = AppConfig.setup_translations()
|
||||||
LxTools.sigi(AppConfig.TEMP_DIR, AppConfig.USER_FILE)
|
LxTools.sigi(AppConfig.TEMP_DIR)
|
||||||
|
|
||||||
window = Wirepy()
|
window = Wirepy()
|
||||||
|
LogConfig.logger(ConfigManager.get("logfile"))
|
||||||
"""
|
"""
|
||||||
the hidden files are hidden in Filedialog
|
the hidden files are hidden in Filedialog
|
||||||
"""
|
"""
|
||||||
@ -1173,5 +1161,5 @@ if __name__ == "__main__":
|
|||||||
window.tk.call("set", "::tk::dialog::file::showHiddenVar", "0")
|
window.tk.call("set", "::tk::dialog::file::showHiddenVar", "0")
|
||||||
window.mainloop()
|
window.mainloop()
|
||||||
|
|
||||||
LxTools.clean_files(AppConfig.TEMP_DIR, AppConfig.USER_FILE)
|
LxTools.clean_files(AppConfig.TEMP_DIR)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
69
wp_app_config.py
Normal file → Executable file
@ -1,15 +1,37 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
"""App configuration for Wire-Py"""
|
"""App configuration for Wire-Py"""
|
||||||
|
import logging
|
||||||
import gettext
|
import gettext
|
||||||
import locale
|
import locale
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import subprocess
|
from subprocess import CompletedProcess, run
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
|
||||||
class AppConfig:
|
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
|
# Localization
|
||||||
APP_NAME: str = "wirepy"
|
APP_NAME: str = "wirepy"
|
||||||
@ -19,7 +41,6 @@ class AppConfig:
|
|||||||
BASE_DIR: Path = Path.home()
|
BASE_DIR: Path = Path.home()
|
||||||
CONFIG_DIR: Path = BASE_DIR / ".config/wire_py"
|
CONFIG_DIR: Path = BASE_DIR / ".config/wire_py"
|
||||||
TEMP_DIR: Path = Path("/tmp/tlecdcwg")
|
TEMP_DIR: Path = Path("/tmp/tlecdcwg")
|
||||||
USER_FILE: Path = Path("/tmp/.log_user")
|
|
||||||
PUBLICKEY: Path = CONFIG_DIR / "pbwgk.pem"
|
PUBLICKEY: Path = CONFIG_DIR / "pbwgk.pem"
|
||||||
|
|
||||||
# Configuration files
|
# Configuration files
|
||||||
@ -31,6 +52,7 @@ class AppConfig:
|
|||||||
"# Theme": "dark",
|
"# Theme": "dark",
|
||||||
"# Tooltips": True,
|
"# Tooltips": True,
|
||||||
"# Autostart": "off",
|
"# Autostart": "off",
|
||||||
|
"# Logfile": LOG_FILE_PATH,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Updates
|
# Updates
|
||||||
@ -42,6 +64,7 @@ class AppConfig:
|
|||||||
# UI configuration
|
# UI configuration
|
||||||
UI_CONFIG: Dict[str, Any] = {
|
UI_CONFIG: Dict[str, Any] = {
|
||||||
"window_title": "Wire-Py",
|
"window_title": "Wire-Py",
|
||||||
|
"window_title2": "LogViewer",
|
||||||
"window_size": (600, 383),
|
"window_size": (600, 383),
|
||||||
"font_family": "Ubuntu",
|
"font_family": "Ubuntu",
|
||||||
"font_size": 11,
|
"font_size": 11,
|
||||||
@ -49,7 +72,7 @@ class AppConfig:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# System-dependent paths
|
# System-dependent paths
|
||||||
SYSTEM_PATHS: Dict[str, str] = {
|
SYSTEM_PATHS: Dict[str, Path] = {
|
||||||
"ssl_decrypt": "/usr/local/bin/ssl_decrypt.py",
|
"ssl_decrypt": "/usr/local/bin/ssl_decrypt.py",
|
||||||
"ssl_encrypt": "/usr/local/bin/ssl_encrypt.py",
|
"ssl_encrypt": "/usr/local/bin/ssl_encrypt.py",
|
||||||
"tcl_path": "/usr/share/TK-Themes",
|
"tcl_path": "/usr/share/TK-Themes",
|
||||||
@ -57,7 +80,7 @@ class AppConfig:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Images and icons paths
|
# 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_vpn": "/usr/share/icons/lx-icons/48/wg_vpn.png",
|
||||||
"icon_msg": "/usr/share/icons/lx-icons/48/wg_msg.png",
|
"icon_msg": "/usr/share/icons/lx-icons/48/wg_msg.png",
|
||||||
"icon_import": "/usr/share/icons/lx-icons/48/wg_import.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_stop": "/usr/share/icons/lx-icons/48/wg_vpn-stop.png",
|
||||||
"icon_info": "/usr/share/icons/lx-icons/64/info.png",
|
"icon_info": "/usr/share/icons/lx-icons/64/info.png",
|
||||||
"icon_error": "/usr/share/icons/lx-icons/64/error.png",
|
"icon_error": "/usr/share/icons/lx-icons/64/error.png",
|
||||||
|
"icon_log": "/usr/share/icons/lx-icons/48/log.png",
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -100,9 +124,9 @@ class AppConfig:
|
|||||||
cls.SETTINGS_FILE.write_text(content)
|
cls.SETTINGS_FILE.write_text(content)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_autostart_content(cls) -> str:
|
def get_autostart_content(cls) -> None:
|
||||||
"""Returns the content for the autostart service file"""
|
"""Returns the content for an autostart service file"""
|
||||||
SYSTEMD_FILE: list[str] = [
|
systemd_file: list[str] = [
|
||||||
"[Unit]",
|
"[Unit]",
|
||||||
"Description=Automatic Tunnel Start",
|
"Description=Automatic Tunnel Start",
|
||||||
"After=network-online.target",
|
"After=network-online.target",
|
||||||
@ -118,27 +142,29 @@ class AppConfig:
|
|||||||
if not cls.SYSTEMD_USER_FOLDER.exists():
|
if not cls.SYSTEMD_USER_FOLDER.exists():
|
||||||
cls.SYSTEMD_USER_FOLDER.mkdir(parents=True, exist_ok=True)
|
cls.SYSTEMD_USER_FOLDER.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
from subprocess import CompletedProcess
|
|
||||||
|
|
||||||
if not cls.AUTOSTART_SERVICE.is_file():
|
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)
|
cls.AUTOSTART_SERVICE.write_text(content)
|
||||||
|
|
||||||
process: CompletedProcess[str] = subprocess.run(
|
process: CompletedProcess[str] = run(
|
||||||
["systemctl", "--user", "enable", "wg_start.service"],
|
["systemctl", "--user", "enable", "wg_start.service"],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
check=False,
|
check=False,
|
||||||
)
|
)
|
||||||
print(process.stdout)
|
|
||||||
if process.returncode == 0:
|
if process.stderr:
|
||||||
print(process.stdout)
|
logging.error(f"{process.stderr} Code: {process.returncode}", exc_info=True)
|
||||||
else:
|
|
||||||
print(f"Error with the following code... {process.returncode}")
|
@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()
|
_ = AppConfig.setup_translations()
|
||||||
|
|
||||||
|
|
||||||
@ -172,6 +198,11 @@ class Msg:
|
|||||||
"imp_err": _("Import Error"),
|
"imp_err": _("Import Error"),
|
||||||
"exp_err": _("Export Error"),
|
"exp_err": _("Export Error"),
|
||||||
"exp_try": _("Export failed! Please try again"),
|
"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"),
|
"tl_first": _("Please first import tunnel"),
|
||||||
"sel_list": _("Please select a tunnel from the list"),
|
"sel_list": _("Please select a tunnel from the list"),
|
||||||
"sign_len": _("The new name may contain only 12 characters"),
|
"sign_len": _("The new name may contain only 12 characters"),
|
||||||
|