556 lines
20 KiB
Python
Executable File
556 lines
20 KiB
Python
Executable File
""" Wireguard Classes and Method for Wire-Py """
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import tkinter as tk
|
|
import zipfile
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from subprocess import check_call
|
|
from tkinter import filedialog, ttk
|
|
import requests
|
|
|
|
|
|
''' 1 = 1. Year, 09 = Month of the Year, 2924 = Day and Year of the Year '''
|
|
version = 'v. 1.10.2024'
|
|
|
|
path_to_file = Path('/etc/wire_py/wg_py')
|
|
path_to_file2 = Path('/etc/wire_py/settings')
|
|
path_to_file3 = Path('/etc/wire_py/theme')
|
|
_u = Path.read_text(Path('/tmp/_u'))
|
|
|
|
UPDATE_API_URL = 'https://git.ilunix.de/api/v1/repos/punix/Wire-Py/releases'
|
|
|
|
|
|
class WirePyUpdate:
|
|
@staticmethod
|
|
def api_down():
|
|
try:
|
|
response = requests.get(UPDATE_API_URL)
|
|
response_dict = response.json()
|
|
response_dict = response_dict[0]
|
|
with open(path_to_file2, 'r') as set_file:
|
|
set_file = set_file.read()
|
|
if 'Update on' in set_file:
|
|
if version[3:] != response_dict['tag_name']:
|
|
return response_dict['tag_name']
|
|
else:
|
|
return 'No Updates'
|
|
else:
|
|
return 'False'
|
|
except requests.exceptions.ConnectionError:
|
|
return 'No Internet Connection!'
|
|
|
|
@staticmethod
|
|
def download():
|
|
try:
|
|
url = f'https://git.ilunix.de/punix/Wire-Py/archive/{res}.zip'
|
|
to_down = 'wget -qP ' + str(_u) + ' ' + url
|
|
result = subprocess.call(to_down, shell=True)
|
|
if result == 0:
|
|
shutil.chown(str(_u) + f'/{res}.zip', 1000, 1000)
|
|
"""img_w, img_i, w_title, w_txt hand over"""
|
|
iw = r'/usr/share/icons/wp-icons/64/info.png'
|
|
ii = r'/usr/share/icons/wp-icons/48/wg_vpn.png'
|
|
wt = 'Download Successful'
|
|
msg_t = 'Your zip file is in home directory'
|
|
msg_window(iw, ii, wt, msg_t)
|
|
|
|
else:
|
|
"""img_w, img_i, w_title, w_txt hand over"""
|
|
iw = r'/usr/share/icons/wp-icons/64/error.png'
|
|
ii = r'/usr/share/icons/wp-icons/48/wg_msg.png'
|
|
wt = 'Download error'
|
|
msg_t = 'Download failed! Please try again'
|
|
msg_window(iw, ii, wt, msg_t)
|
|
except subprocess.CalledProcessError:
|
|
"""img_w, img_i, w_title, w_txt hand over"""
|
|
iw = r'/usr/share/icons/wp-icons/64/error.png'
|
|
ii = r'/usr/share/icons/wp-icons/48/wg_msg.png'
|
|
wt = 'Download error'
|
|
msg_t = 'Download failed! No internet connection!'
|
|
msg_window(iw, ii, wt, msg_t)
|
|
|
|
|
|
res = WirePyUpdate.api_down()
|
|
|
|
|
|
def msg_window(img_w, img_i, w_title, w_txt):
|
|
"""
|
|
Function for different message windows for the user. with 4 arguments to be passed.
|
|
To create messages with your own images, icons, and titles. As an alternative to Python Messagebox.
|
|
Paths to images must be specified: r'/usr/share/icons/wp-icons/64/info.png'
|
|
img_w = Image for Tk Window
|
|
img_i = Image for Icon
|
|
w_title = Windows Title
|
|
w_txt = Text for Tk Window
|
|
"""
|
|
|
|
msg = tk.Toplevel()
|
|
msg.resizable(width=False, height=False)
|
|
msg.title(w_title)
|
|
msg.configure(pady=15, padx=15)
|
|
msg.img = tk.PhotoImage(file=img_w)
|
|
msg.i_window = tk.Label(msg, image=msg.img)
|
|
msg.i_window.grid(column=0, row=0)
|
|
label = tk.Label(msg, text=w_txt)
|
|
label.config(font=('Ubuntu', 11), padx=15)
|
|
label.grid(column=1, row=0)
|
|
button = ttk.Button(msg, text='OK', command=msg.destroy, padding=4)
|
|
button.config()
|
|
button.grid(column=0, columnspan=2, row=1)
|
|
img_i = tk.PhotoImage(file=img_i)
|
|
msg.iconphoto(True, img_i)
|
|
msg.columnconfigure(0, weight=1)
|
|
msg.rowconfigure(0, weight=1)
|
|
msg.winfo_toplevel()
|
|
|
|
|
|
class GreenLabel:
|
|
"""
|
|
Show the active tunnel in green in the label
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.StrVar = None
|
|
self.lb_tunnel = None
|
|
|
|
def green_show_label(self):
|
|
with open(path_to_file3, 'r') as read_file:
|
|
if 'light' in read_file:
|
|
self.lb_tunnel = ttk.Label(self, textvariable=self.StrVar, foreground='green')
|
|
|
|
else:
|
|
self.lb_tunnel = ttk.Label(self, textvariable=self.StrVar, foreground='yellow')
|
|
|
|
self.lb_tunnel.config(font=('Ubuntu', 11, 'bold'))
|
|
self.lb_tunnel.grid(column=2, padx=10, row=1)
|
|
self.columnconfigure(2, weight=1)
|
|
self.rowconfigure(0, weight=1)
|
|
|
|
def columnconfigure(self, param, weight):
|
|
pass
|
|
|
|
def rowconfigure(self, param, weight):
|
|
pass
|
|
|
|
class StartStopBTN:
|
|
"""
|
|
Show Start and Stop Button in Label
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.lb_frame_btn_lbox = None
|
|
self.wg_switch = None
|
|
self.btn_stst = None
|
|
self.wg_vpn_start = tk.PhotoImage(file=r'/usr/share/icons/wp-icons/48/wg_vpn-start.png')
|
|
self.wg_vpn_stop = tk.PhotoImage(file=r'/usr/share/icons/wp-icons/48/wg_vpn-stop.png')
|
|
|
|
def button_stop(self):
|
|
self.btn_stst = ttk.Button(self.lb_frame_btn_lbox, image=self.wg_vpn_stop, command=self.wg_switch, padding=0)
|
|
self.btn_stst.grid(column=0, row=0, padx=5, pady=8)
|
|
|
|
def button_start(self):
|
|
self.btn_stst = ttk.Button(self.lb_frame_btn_lbox, image=self.wg_vpn_start, command=self.wg_switch, padding=0)
|
|
self.btn_stst.grid(column=0, row=0, padx=5, pady=8)
|
|
|
|
class ConToDict:
|
|
"""
|
|
The config file is packed into a dictionary,
|
|
to display the values Address , DNS and Peer in the labels
|
|
"""
|
|
|
|
@classmethod
|
|
def covert_to_dict(cls, file):
|
|
|
|
dictlist = []
|
|
for lines in file.readlines():
|
|
line_plit = lines.split()
|
|
dictlist = dictlist + line_plit
|
|
dictlist.remove('[Interface]')
|
|
dictlist.remove('[Peer]')
|
|
for items in dictlist:
|
|
if items == '=':
|
|
dictlist.remove(items)
|
|
|
|
''' Here is the beginning (Loop) of convert List to Dictionary '''
|
|
for _ in dictlist:
|
|
a = [dictlist[0], dictlist[1]]
|
|
b = [dictlist[2], dictlist[3]]
|
|
c = [dictlist[4], dictlist[5]]
|
|
d = [dictlist[6], dictlist[7]]
|
|
e = [dictlist[8], dictlist[9]]
|
|
f = [dictlist[10], dictlist[11]]
|
|
g = [dictlist[12], dictlist[13]]
|
|
h = [dictlist[14], dictlist[15]]
|
|
new_list = [a, b, c, d, e, f, g, h]
|
|
final_dict = {}
|
|
for elements in new_list:
|
|
final_dict[elements[0]] = elements[1]
|
|
|
|
''' end... result a Dictionary '''
|
|
|
|
address = final_dict['Address']
|
|
dns = final_dict['DNS']
|
|
if ',' in dns:
|
|
dns = dns[:-1]
|
|
endpoint = final_dict['Endpoint']
|
|
if 'PresharedKey' in final_dict:
|
|
pre_key = final_dict['PresharedKey']
|
|
else:
|
|
pre_key = final_dict['PreSharedKey']
|
|
return address, dns, endpoint, pre_key
|
|
|
|
|
|
class TunnelActiv:
|
|
"""
|
|
Shows the Active Tunnel
|
|
"""
|
|
|
|
@staticmethod
|
|
def active():
|
|
|
|
active = os.popen('nmcli con show --active | grep -iPo "(.*)(wireguard)"').read().split()
|
|
if not active:
|
|
active = ''
|
|
else:
|
|
active = active[0]
|
|
|
|
return active
|
|
|
|
|
|
class ShowAddress:
|
|
"""
|
|
Displays the value address, DNS and peer in the labels
|
|
or empty it again
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.lb_frame2 = None
|
|
self.lb_frame = None
|
|
self.endpoint = None
|
|
self.dns = None
|
|
self.address = None
|
|
self.enp = None
|
|
self.DNS = None
|
|
self.add = None
|
|
|
|
def init_and_report(self, data=None):
|
|
""" Address Label """
|
|
self.add = tk.StringVar()
|
|
self.add.set('Address: ' + data[0])
|
|
self.DNS = tk.StringVar()
|
|
self.DNS.set(' DNS: ' + data[1])
|
|
self.enp = tk.StringVar()
|
|
self.enp.set('Endpoint: ' + data[2])
|
|
|
|
def label_empty(self):
|
|
self.add.set('')
|
|
self.DNS.set('')
|
|
self.enp.set('')
|
|
|
|
def show_data(self):
|
|
""" Address Label """
|
|
self.address = ttk.Label(self.lb_frame, textvariable=self.add, foreground='#0071ff')
|
|
self.address.grid(column=0, row=5, sticky='w', padx=10, pady=6)
|
|
self.address.config(font=('Ubuntu', 9))
|
|
|
|
''' DNS Label '''
|
|
self.dns = ttk.Label(self.lb_frame, textvariable=self.DNS, foreground='#0071ff')
|
|
self.dns.grid(column=0, row=7, sticky='w', padx=10, pady=6)
|
|
self.dns.config(font=('Ubuntu', 9))
|
|
|
|
''' Endpoint Label '''
|
|
self.endpoint = ttk.Label(self.lb_frame2, textvariable=self.enp, foreground='#0071ff')
|
|
self.endpoint.grid(column=0, row=8, sticky='w', padx=10, pady=20)
|
|
self.endpoint.config(font=('Ubuntu', 9))
|
|
|
|
|
|
class ListTunnels:
|
|
"""
|
|
Shows all existing Wireguard tunnels
|
|
"""
|
|
|
|
@staticmethod
|
|
def tl_list():
|
|
wg_s = os.popen('nmcli con show | grep -iPo "(.*)(wireguard)"').read().split()
|
|
|
|
''' tl = Tunnel list # Show of 4.Element in list '''
|
|
tl = wg_s[::3]
|
|
return tl
|
|
|
|
|
|
class ImportTunnel:
|
|
"""
|
|
Import Class for Wireguard config Files.
|
|
Before importing, it is checked whether PrivateKey and PublicKey are in the file.
|
|
If True then it is checked whether the PreSharedKey is already in the key file
|
|
to avoid an import error so that no double wgconf are imported.
|
|
Thus, tunnels can be renamed without the problems arise. If False then the key is written into the file.
|
|
Furthermore, it is checked whether the name is longer than 12 characters.
|
|
If True then the name is automatically shortened to 12 characters and then imported.
|
|
If in each case false comes out, a corresponding window comes to inform the user that something is wrong.
|
|
"""
|
|
|
|
def __init__(self):
|
|
|
|
self.select_tunnel = None
|
|
self.wg_switch = None
|
|
self.btn_stst = None
|
|
self.lb_tunnel = None
|
|
self.StrVar = None
|
|
self.a = None
|
|
self.l_box = None
|
|
|
|
def wg_import_select(self):
|
|
|
|
try:
|
|
filepath = filedialog.askopenfilename(initialdir=str(_u), title='Select Wireguard config File',
|
|
filetypes=[('WG config files', '*.conf')], )
|
|
|
|
with open(filepath, 'r') as file:
|
|
read = file.read()
|
|
path_split = filepath.split('/')
|
|
path_split1 = path_split[-1]
|
|
self.a = TunnelActiv.active()
|
|
|
|
if 'PrivateKey = ' in read and 'PublicKey = 'in read and 'Endpoint =' in read:
|
|
with open(filepath, 'r') as file:
|
|
key = ConToDict.covert_to_dict(file)
|
|
pre_key = key[3]
|
|
if len(pre_key) != 0:
|
|
with open('/etc/wire_py/.keys', 'r') as readfile:
|
|
p_key = readfile.readlines()
|
|
if pre_key in p_key or pre_key + '\n' in p_key:
|
|
"""img_w, img_i, w_title, w_txt hand over"""
|
|
iw = r'/usr/share/icons/wp-icons/64/error.png'
|
|
ii = r'/usr/share/icons/wp-icons/48/wg_msg.png'
|
|
wt = 'Import Error'
|
|
msg_t = 'Tunnel already available!\nPlease use another file for import'
|
|
msg_window(iw, ii, wt, msg_t)
|
|
|
|
else:
|
|
|
|
with open('/etc/wire_py/.keys', 'a') as keyfile:
|
|
keyfile.write(pre_key + '\r')
|
|
if len(path_split1) > 17:
|
|
p1 = shutil.copy(filepath, Path('/etc/wire_py/'))
|
|
path_split = path_split1[len(path_split1) - 17:]
|
|
os.rename(p1, Path('/etc/wire_py') / str(path_split))
|
|
new_conf = '/etc/wire_py/' + path_split
|
|
if self.a != '':
|
|
check_call(['nmcli', 'connection', 'down', TunnelActiv.active()])
|
|
ShowAddress.label_empty(self)
|
|
|
|
subprocess.check_output(['nmcli', 'connection', 'import', 'type',
|
|
'wireguard', 'file', new_conf], text=True)
|
|
|
|
else:
|
|
shutil.copy(filepath, Path('/etc/wire_py/'))
|
|
if self.a != '':
|
|
check_call(['nmcli', 'connection', 'down', TunnelActiv.active()])
|
|
ShowAddress.label_empty(self)
|
|
|
|
subprocess.check_output(['nmcli', 'connection', 'import', 'type',
|
|
'wireguard', 'file', filepath], text=True)
|
|
|
|
self.StrVar.set('')
|
|
self.a = TunnelActiv.active()
|
|
self.l_box.insert(0, self.a)
|
|
self.l_box.update()
|
|
self.StrVar = tk.StringVar()
|
|
self.StrVar.set(self.a)
|
|
GreenLabel.green_show_label(self)
|
|
StartStopBTN.button_stop(self)
|
|
wg_read = Path('/etc/wire_py') / str(self.a + '.conf')
|
|
with open(wg_read, 'r') as file_for_key:
|
|
data = ConToDict.covert_to_dict(file_for_key)
|
|
|
|
''' Address Label '''
|
|
ShowAddress.init_and_report(self, data)
|
|
ShowAddress.show_data(self)
|
|
check_call(['nmcli', 'con', 'mod', self.a, 'connection.autoconnect', 'no'])
|
|
Path.chmod(wg_read, 0o600)
|
|
|
|
if 'PrivateKey = ' and 'Endpoint = ' not in read:
|
|
"""img_w, img_i, w_title, w_txt hand over"""
|
|
iw = r'/usr/share/icons/wp-icons/64/error.png'
|
|
ii = r'/usr/share/icons/wp-icons/48/wg_msg.png'
|
|
wt = 'Import Error'
|
|
msg_t = 'Oh... no valid Wireguard File!\nPlease select a valid Wireguard File'
|
|
msg_window(iw, ii, wt, msg_t)
|
|
|
|
except EOFError:
|
|
pass
|
|
except TypeError:
|
|
pass
|
|
except FileNotFoundError:
|
|
pass
|
|
except subprocess.CalledProcessError:
|
|
|
|
print('Tunnel exist!')
|
|
|
|
|
|
class FileHandle:
|
|
"""
|
|
This class will display the autostart label which
|
|
Tunnel is automatically started regardless of the active tunnel.
|
|
The selected tunnel is written into a file to read it after the start of the system.
|
|
"""
|
|
|
|
def __init__(self):
|
|
|
|
self.wg_autostart = None
|
|
self.autoconnect = None
|
|
self.auto_con = None
|
|
self.autoconnect_var = None
|
|
self.tl = None
|
|
self.selected_option = None
|
|
self.l_box = None
|
|
|
|
def box_set(self):
|
|
|
|
try:
|
|
select_tunnel = self.l_box.curselection()
|
|
select_tl = self.l_box.get(select_tunnel[0])
|
|
|
|
if self.selected_option.get() == 0:
|
|
Path.unlink(path_to_file)
|
|
tl = ListTunnels.tl_list()
|
|
|
|
if len(tl) == 0:
|
|
self.wg_autostart.configure(state='disabled')
|
|
|
|
if self.selected_option.get() >= 1:
|
|
Path.write_text(path_to_file, select_tl)
|
|
|
|
except IndexError:
|
|
self.selected_option.set(1)
|
|
|
|
OnOff.on_off(self)
|
|
|
|
|
|
class OnOff:
|
|
"""
|
|
Here it is checked whether the path to the file is there if not it is created.
|
|
Set (on), the selected tunnel is displayed in the label.
|
|
At (off) the label is first emptied then filled with No Autoconnect
|
|
"""
|
|
|
|
def __init__(self):
|
|
|
|
self.wg_autostart = None
|
|
self.selected_option = None
|
|
self.auto_con = None
|
|
self.autoconnect = None
|
|
self.autoconnect_var = None
|
|
self.lb_frame_buttons = None
|
|
|
|
def on_off(self):
|
|
|
|
if Path.exists(path_to_file):
|
|
self.selected_option.set(1)
|
|
self.autoconnect_var.set('')
|
|
if not Path.is_dir(Path('/etc/wire_py')):
|
|
Path.mkdir(Path('/etc/wire_py'))
|
|
self.auto_con = Path.read_text(path_to_file)
|
|
|
|
else:
|
|
|
|
self.wg_autostart.configure(state='disabled')
|
|
self.auto_con = 'no Autoconnect'
|
|
self.autoconnect_var.set('')
|
|
self.autoconnect_var = tk.StringVar()
|
|
self.autoconnect_var.set(self.auto_con)
|
|
|
|
self.autoconnect = ttk.Label(self, textvariable=self.autoconnect_var, foreground='#0071ff')
|
|
self.autoconnect.config(font=('Ubuntu', 11))
|
|
self.autoconnect.grid(column=0, row=4, sticky='ne', pady=19)
|
|
|
|
|
|
class ExportTunnels:
|
|
"""
|
|
This will export the tunnels.
|
|
A zipfile with current date and time is created
|
|
in the user's home directory with correct right
|
|
"""
|
|
|
|
@staticmethod
|
|
def wg_export():
|
|
_u1 = str(_u[6:])
|
|
now_time = datetime.now()
|
|
now_datetime = now_time.strftime('wg-exp-' + '%m-%d-%Y' + '-' + '%H:%M')
|
|
tl = ListTunnels.tl_list()
|
|
|
|
try:
|
|
if len(tl) != 0:
|
|
wg_tar = str(_u) + '/' + now_datetime
|
|
shutil.copytree('/etc/wire_py', '/tmp/wire_py', dirs_exist_ok=True)
|
|
source = Path('/tmp/wire_py')
|
|
Path.unlink(Path(source) / 'wg_py', missing_ok=True)
|
|
Path.unlink(Path(source) / '.keys', missing_ok=True)
|
|
Path.unlink(Path(source) / 'settings', missing_ok=True)
|
|
Path.unlink(Path(source) / 'theme', missing_ok=True)
|
|
shutil.make_archive(wg_tar, 'zip', source)
|
|
shutil.chown(wg_tar + '.zip', 1000, 1000)
|
|
shutil.rmtree(source)
|
|
with zipfile.ZipFile((wg_tar + '.zip'), 'r') as zf:
|
|
if len(zf.namelist()) != 0:
|
|
|
|
"""img_w, img_i, w_title, w_txt hand over"""
|
|
iw = r'/usr/share/icons/wp-icons/64/info.png'
|
|
ii = r'/usr/share/icons/wp-icons/48/wg_vpn.png'
|
|
wt = 'Export Successful'
|
|
msg_t = 'Your zip file is in home directory'
|
|
msg_window(iw, ii, wt, msg_t)
|
|
|
|
else:
|
|
|
|
"""img_w, img_i, w_title, w_txt hand over"""
|
|
iw = r'/usr/share/icons/wp-icons/64/error.png'
|
|
ii = r'/usr/share/icons/wp-icons/48/wg_msg.png'
|
|
wt = 'Export error'
|
|
msg_t = 'Export failed! Please try again'
|
|
msg_window(iw, ii, wt, msg_t)
|
|
|
|
else:
|
|
|
|
"""img_w, img_i, w_title, w_txt hand over"""
|
|
iw = r'/usr/share/icons/wp-icons/64/info.png'
|
|
ii = r'/usr/share/icons/wp-icons/48/wg_msg.png'
|
|
wt = 'Select tunnel'
|
|
msg_t = 'Please first import tunnel.'
|
|
msg_window(iw, ii, wt, msg_t)
|
|
|
|
except TypeError:
|
|
pass
|
|
|
|
|
|
class MyToolTip(tk.Toplevel):
|
|
TIP_X_OFFSET = 8
|
|
TIP_Y_OFFSET = 8
|
|
AUTO_CLEAR_TIME = 10 # Millisecond. (1/100 sec.)
|
|
|
|
def __init__(self, x_pos, y_pos, message=None, auto_clear=False):
|
|
self.x_pos = x_pos
|
|
self.y_pos = y_pos
|
|
self.message = message
|
|
self.auto_clear = auto_clear
|
|
|
|
tk.Toplevel.__init__(self)
|
|
self.overrideredirect(True)
|
|
|
|
self.message_label = ttk.Label(self, compound='left', text=self.message, padding=4)
|
|
self.message_label.pack()
|
|
|
|
self.geometry("+%d+%d" % (self.x_pos + self.TIP_X_OFFSET,
|
|
self.y_pos + self.TIP_X_OFFSET))
|
|
|
|
if self.auto_clear:
|
|
self.after(self.AUTO_CLEAR_TIME, self.clear_tip)
|
|
|
|
def clear_tip(self):
|
|
"""Remove Tool-Tip"""
|
|
self.destroy()
|